mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-28 03:49:22 +08:00
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
389 lines
12 KiB
TypeScript
389 lines
12 KiB
TypeScript
// Package Utilities
|
|
import { graphql } from 'gatsby';
|
|
import React, { Component } from 'react';
|
|
import Helmet from 'react-helmet';
|
|
import type { TFunction } from 'i18next';
|
|
import { Trans, withTranslation } from 'react-i18next';
|
|
import { connect } from 'react-redux';
|
|
import { bindActionCreators } from 'redux';
|
|
import type { Dispatch } from 'redux';
|
|
import { createSelector } from 'reselect';
|
|
import { Container, Col, Row, Alert, Button } from '@freecodecamp/ui';
|
|
|
|
// Local Utilities
|
|
import Spacer from '../../../components/helpers/spacer';
|
|
import LearnLayout from '../../../components/layouts/learn';
|
|
import ChallengeTitle from '../components/challenge-title';
|
|
import ChallengeHeading from '../components/challenge-heading';
|
|
import PrismFormatted from '../components/prism-formatted';
|
|
import { challengeTypes } from '../../../../../shared/config/challenge-types';
|
|
import CompletionModal from '../components/completion-modal';
|
|
import HelpModal from '../components/help-modal';
|
|
import Hotkeys from '../components/hotkeys';
|
|
import { hideCodeAlly, tryToShowCodeAlly } from '../../../redux/actions';
|
|
import {
|
|
completedChallengesSelector,
|
|
partiallyCompletedChallengesSelector,
|
|
showCodeAllySelector,
|
|
isSignedInSelector,
|
|
userTokenSelector
|
|
} from '../../../redux/selectors';
|
|
import {
|
|
challengeMounted,
|
|
updateChallengeMeta,
|
|
openModal,
|
|
updateSolutionFormValues
|
|
} from '../redux/actions';
|
|
import { isChallengeCompletedSelector } from '../redux/selectors';
|
|
import { createFlashMessage } from '../../../components/Flash/redux';
|
|
import {
|
|
ChallengeNode,
|
|
ChallengeMeta,
|
|
CompletedChallenge
|
|
} from '../../../redux/prop-types';
|
|
import ProjectToolPanel from '../projects/tool-panel';
|
|
import SolutionForm from '../projects/solution-form';
|
|
import { FlashMessages } from '../../../components/Flash/redux/flash-messages';
|
|
import { SuperBlocks } from '../../../../../shared/config/superblocks';
|
|
import { CodeAllyDown } from '../../../components/growth-book/codeally-down';
|
|
|
|
import './codeally.css';
|
|
|
|
// Redux
|
|
const mapStateToProps = createSelector(
|
|
completedChallengesSelector,
|
|
isChallengeCompletedSelector,
|
|
isSignedInSelector,
|
|
partiallyCompletedChallengesSelector,
|
|
showCodeAllySelector,
|
|
userTokenSelector,
|
|
(
|
|
completedChallenges: CompletedChallenge[],
|
|
isChallengeCompleted: boolean,
|
|
isSignedIn: boolean,
|
|
partiallyCompletedChallenges: CompletedChallenge[],
|
|
showCodeAlly: boolean,
|
|
userToken: string | null
|
|
) => ({
|
|
completedChallenges,
|
|
isChallengeCompleted,
|
|
isSignedIn,
|
|
partiallyCompletedChallenges,
|
|
showCodeAlly,
|
|
userToken
|
|
})
|
|
);
|
|
|
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
|
bindActionCreators(
|
|
{
|
|
challengeMounted,
|
|
createFlashMessage,
|
|
hideCodeAlly,
|
|
openCompletionModal: () => openModal('completion'),
|
|
tryToShowCodeAlly,
|
|
updateChallengeMeta,
|
|
updateSolutionFormValues
|
|
},
|
|
dispatch
|
|
);
|
|
|
|
// Types
|
|
interface ShowCodeAllyProps {
|
|
challengeMounted: (arg0: string) => void;
|
|
completedChallenges: CompletedChallenge[];
|
|
createFlashMessage: typeof createFlashMessage;
|
|
data: { challengeNode: ChallengeNode };
|
|
hideCodeAlly: () => void;
|
|
isChallengeCompleted: boolean;
|
|
isSignedIn: boolean;
|
|
openCompletionModal: () => void;
|
|
pageContext: {
|
|
challengeMeta: ChallengeMeta;
|
|
};
|
|
partiallyCompletedChallenges: CompletedChallenge[];
|
|
showCodeAlly: boolean;
|
|
t: TFunction;
|
|
tryToShowCodeAlly: () => void;
|
|
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
|
updateSolutionFormValues: () => void;
|
|
userToken: string | null;
|
|
}
|
|
|
|
class ShowCodeAlly extends Component<ShowCodeAllyProps> {
|
|
static displayName: string;
|
|
private container: React.RefObject<HTMLElement> = React.createRef();
|
|
|
|
componentDidMount(): void {
|
|
const {
|
|
challengeMounted,
|
|
data: {
|
|
challengeNode: {
|
|
challenge: { challengeType, helpCategory, title }
|
|
}
|
|
},
|
|
pageContext: { challengeMeta },
|
|
updateChallengeMeta
|
|
} = this.props;
|
|
updateChallengeMeta({
|
|
...challengeMeta,
|
|
title,
|
|
challengeType,
|
|
helpCategory
|
|
});
|
|
challengeMounted(challengeMeta.id);
|
|
this.container.current?.focus();
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this.props.hideCodeAlly();
|
|
}
|
|
|
|
handleSubmit = ({
|
|
showCompletionModal
|
|
}: {
|
|
showCompletionModal: boolean;
|
|
}) => {
|
|
const {
|
|
completedChallenges,
|
|
createFlashMessage,
|
|
data: {
|
|
challengeNode: {
|
|
challenge: { id: challengeId }
|
|
}
|
|
},
|
|
openCompletionModal,
|
|
partiallyCompletedChallenges
|
|
} = this.props;
|
|
|
|
const isPartiallyCompleted = partiallyCompletedChallenges.some(
|
|
challenge => challenge.id === challengeId
|
|
);
|
|
|
|
const isCompleted = completedChallenges.some(
|
|
challenge => challenge.id === challengeId
|
|
);
|
|
|
|
if (!isPartiallyCompleted && !isCompleted) {
|
|
createFlashMessage({
|
|
type: 'danger',
|
|
message: FlashMessages.CompleteProjectFirst
|
|
});
|
|
} else if (showCompletionModal) {
|
|
openCompletionModal();
|
|
}
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
completedChallenges,
|
|
data: {
|
|
challengeNode: {
|
|
challenge: {
|
|
challengeType,
|
|
description,
|
|
fields: { blockName },
|
|
id: challengeId,
|
|
instructions,
|
|
notes,
|
|
superBlock,
|
|
title,
|
|
translationPending,
|
|
url
|
|
}
|
|
}
|
|
},
|
|
isChallengeCompleted,
|
|
isSignedIn,
|
|
pageContext: {
|
|
challengeMeta: { nextChallengePath, prevChallengePath }
|
|
},
|
|
partiallyCompletedChallenges,
|
|
showCodeAlly,
|
|
t,
|
|
tryToShowCodeAlly,
|
|
updateSolutionFormValues,
|
|
userToken = null
|
|
} = this.props;
|
|
|
|
const blockNameTitle = `${t(
|
|
`intro:${superBlock}.blocks.${blockName}.title`
|
|
)}: ${title}`;
|
|
const windowTitle = `${blockNameTitle} | freeCodeCamp.org`;
|
|
|
|
// Initial CodeAlly login includes a tempToken in redirect URL
|
|
const queryParams = new URLSearchParams(window.location.search);
|
|
const codeAllyTempToken: string | null = queryParams.get('tempToken');
|
|
|
|
const tempToken = codeAllyTempToken ? `tempToken=${codeAllyTempToken}` : '';
|
|
|
|
// Include a unique param to avoid CodeAlly caching issues
|
|
const date = `date=${Date.now()}`;
|
|
|
|
// User token for submitting CodeRoad tutorials
|
|
const envVariables = userToken
|
|
? `envVariables=CODEROAD_WEBHOOK_TOKEN=${userToken}`
|
|
: '';
|
|
|
|
const goBackTo = `goBackTo=${window.location.href}`;
|
|
|
|
const isPartiallyCompleted = partiallyCompletedChallenges.some(
|
|
challenge => challenge.id === challengeId
|
|
);
|
|
|
|
const isCompleted = completedChallenges.some(
|
|
challenge => challenge.id === challengeId
|
|
);
|
|
const titleContext = t('learn.source-code-link');
|
|
return showCodeAlly ? (
|
|
<LearnLayout>
|
|
<Helmet title={windowTitle} />
|
|
<iframe
|
|
className='codeally-frame'
|
|
data-cy='codeally-frame'
|
|
name={`codeAlly${Date.now()}`}
|
|
sandbox='allow-modals allow-forms allow-popups allow-scripts allow-same-origin'
|
|
src={`https://codeally.io/embed/?repoUrl=${url}&${goBackTo}&${envVariables}&${tempToken}&${date}`}
|
|
title='Editor'
|
|
/>
|
|
</LearnLayout>
|
|
) : (
|
|
<Hotkeys
|
|
containerRef={this.container}
|
|
nextChallengePath={nextChallengePath}
|
|
prevChallengePath={prevChallengePath}
|
|
>
|
|
<LearnLayout>
|
|
<Helmet title={windowTitle} />
|
|
<Container>
|
|
<Row>
|
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
|
<Spacer size='medium' />
|
|
{superBlock === SuperBlocks.RelationalDb && <CodeAllyDown />}
|
|
<Spacer size='medium' />
|
|
<ChallengeTitle
|
|
isCompleted={isChallengeCompleted}
|
|
translationPending={translationPending}
|
|
>
|
|
{title}
|
|
</ChallengeTitle>
|
|
<Spacer size='medium' />
|
|
<PrismFormatted text={description} />
|
|
<Spacer size='medium' />
|
|
<div className='ca-description'>
|
|
<Trans i18nKey='learn.github-required'>
|
|
<a
|
|
href='https://github.com/join'
|
|
rel='noopener noreferrer'
|
|
target='_blank'
|
|
title={titleContext}
|
|
>
|
|
placeholder
|
|
</a>
|
|
</Trans>
|
|
</div>
|
|
<Spacer size='medium' />
|
|
{isSignedIn &&
|
|
challengeType === challengeTypes.codeAllyCert && (
|
|
<>
|
|
<div className='ca-description'>
|
|
{t('learn.complete-both-steps')}
|
|
</div>
|
|
<hr />
|
|
<Spacer size='medium' />
|
|
<ChallengeHeading
|
|
heading={t('learn.step-1')}
|
|
isCompleted={isPartiallyCompleted || isCompleted}
|
|
/>
|
|
<Spacer size='medium' />
|
|
<div className='ca-description'>
|
|
{t('learn.runs-in-vm')}
|
|
</div>
|
|
<Spacer size='medium' />
|
|
<PrismFormatted text={instructions} />
|
|
<Spacer size='medium' />
|
|
</>
|
|
)}
|
|
<Alert id='codeally-cookie-warning' variant='info'>
|
|
<p>{t(`intro:misc-text.enable-cookies`)}</p>
|
|
</Alert>
|
|
<Button
|
|
aria-describedby='codeally-cookie-warning'
|
|
block={true}
|
|
variant='primary'
|
|
data-cy='start-codeally'
|
|
onClick={tryToShowCodeAlly}
|
|
>
|
|
{challengeType === challengeTypes.codeAllyCert
|
|
? t('buttons.click-start-project')
|
|
: t('buttons.click-start-course')}
|
|
</Button>
|
|
{isSignedIn &&
|
|
challengeType === challengeTypes.codeAllyCert && (
|
|
<>
|
|
<hr />
|
|
<Spacer size='medium' />
|
|
<ChallengeHeading
|
|
heading={t('learn.step-2')}
|
|
isCompleted={isCompleted}
|
|
/>
|
|
<Spacer size='medium' />
|
|
<div className='ca-description'>
|
|
{t('learn.submit-public-url')}
|
|
</div>
|
|
<Spacer size='medium' />
|
|
<PrismFormatted text={notes} />
|
|
<Spacer size='medium' />
|
|
<SolutionForm
|
|
challengeType={challengeType}
|
|
description={description}
|
|
onSubmit={this.handleSubmit}
|
|
updateSolutionForm={updateSolutionFormValues}
|
|
/>
|
|
</>
|
|
)}
|
|
<Spacer size='xxSmall' />
|
|
<ProjectToolPanel />
|
|
<br />
|
|
<Spacer size='medium' />
|
|
</Col>
|
|
<CompletionModal />
|
|
<HelpModal challengeTitle={title} challengeBlock={blockName} />
|
|
</Row>
|
|
</Container>
|
|
</LearnLayout>
|
|
</Hotkeys>
|
|
);
|
|
}
|
|
}
|
|
|
|
ShowCodeAlly.displayName = 'ShowCodeAlly';
|
|
|
|
export default connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps
|
|
)(withTranslation()(ShowCodeAlly));
|
|
|
|
// GraphQL
|
|
export const query = graphql`
|
|
query CodeAllyChallenge($slug: String!) {
|
|
challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
|
|
challenge {
|
|
challengeType
|
|
description
|
|
fields {
|
|
blockName
|
|
}
|
|
helpCategory
|
|
id
|
|
instructions
|
|
notes
|
|
superBlock
|
|
title
|
|
translationPending
|
|
url
|
|
}
|
|
}
|
|
}
|
|
`;
|