feat: show project preview for first challenge

This commit is contained in:
Oliver Eyton-Williams
2021-10-21 14:39:17 +02:00
committed by moT01
parent cebda66b04
commit 943aa975d3
6 changed files with 147 additions and 31 deletions

View File

@ -62,7 +62,8 @@
"verify-email": "Verify Email", "verify-email": "Verify Email",
"submit-and-go": "Submit and go to next challenge", "submit-and-go": "Submit and go to next challenge",
"go-to-next": "Go to next challenge", "go-to-next": "Go to next challenge",
"ask-later": "Ask me later" "ask-later": "Ask me later",
"hide-project-preview": "Get started"
}, },
"landing": { "landing": {
"big-heading-1": "Learn to code — for free.", "big-heading-1": "Learn to code — for free.",
@ -288,7 +289,8 @@
"preview": "Preview" "preview": "Preview"
}, },
"help-translate": "We are still translating the following certifications.", "help-translate": "We are still translating the following certifications.",
"help-translate-link": "Help us translate." "help-translate-link": "Help us translate.",
"project-preview-title": "Complete project demo."
}, },
"donate": { "donate": {
"title": "Support our nonprofit", "title": "Support our nonprofit",

View File

@ -105,7 +105,7 @@ export type MarkdownRemark = {
type Question = { text: string; answers: string[]; solution: number }; type Question = { text: string; answers: string[]; solution: number };
type Fields = { slug: string; blockName: string; tests: Test[] }; type Fields = { slug: string; blockName: string; tests: Test[] };
type Required = { export type Required = {
link: string; link: string;
raw: boolean; raw: boolean;
src: string; src: string;

View File

@ -28,6 +28,9 @@ import CompletionModal from '../components/completion-modal';
import HelpModal from '../components/help-modal'; import HelpModal from '../components/help-modal';
import Output from '../components/output'; import Output from '../components/output';
import Preview from '../components/preview'; import Preview from '../components/preview';
import ProjectPreviewModal, {
PreviewConfig
} from '../components/project-preview-modal';
import SidePanel from '../components/side-panel'; import SidePanel from '../components/side-panel';
import VideoModal from '../components/video-modal'; import VideoModal from '../components/video-modal';
import { import {
@ -41,7 +44,10 @@ import {
initConsole, initConsole,
initTests, initTests,
isChallengeCompletedSelector, isChallengeCompletedSelector,
updateChallengeMeta previewMounted,
updateChallengeMeta,
openModal,
setEditorFocusability
} from '../redux'; } from '../redux';
import { getGuideUrl } from '../utils'; import { getGuideUrl } from '../utils';
import MultifileEditor from './MultifileEditor'; import MultifileEditor from './MultifileEditor';
@ -68,7 +74,10 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
updateChallengeMeta, updateChallengeMeta,
challengeMounted, challengeMounted,
executeChallenge, executeChallenge,
cancelTests cancelTests,
previewMounted,
openModal,
setEditorFocusability
}, },
dispatch dispatch
); );
@ -87,10 +96,13 @@ interface ShowClassicProps {
output: string[]; output: string[];
pageContext: { pageContext: {
challengeMeta: ChallengeMeta; challengeMeta: ChallengeMeta;
projectPreview: PreviewConfig & { isFirstChallengeInBlock: boolean };
}; };
t: TFunction; t: TFunction;
tests: Test[]; tests: Test[];
updateChallengeMeta: (arg0: ChallengeMeta) => void; updateChallengeMeta: (arg0: ChallengeMeta) => void;
openModal: (modal: string) => void;
setEditorFocusability: (canFocus: boolean) => void;
} }
interface ShowClassicState { interface ShowClassicState {
@ -231,6 +243,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
initConsole, initConsole,
initTests, initTests,
updateChallengeMeta, updateChallengeMeta,
openModal,
data: { data: {
challengeNode: { challengeNode: {
challengeFiles, challengeFiles,
@ -240,11 +253,15 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
helpCategory helpCategory
} }
}, },
pageContext: { challengeMeta } pageContext: {
challengeMeta,
projectPreview: { isFirstChallengeInBlock }
}
} = this.props; } = this.props;
initConsole(''); initConsole('');
createFiles(challengeFiles ?? []); createFiles(challengeFiles ?? []);
initTests(tests); initTests(tests);
if (isFirstChallengeInBlock) openModal('projectPreview');
updateChallengeMeta({ updateChallengeMeta({
...challengeMeta, ...challengeMeta,
title, title,
@ -359,7 +376,11 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
renderPreview() { renderPreview() {
return ( return (
<Preview className='full-height' disableIframe={this.state.resizing} /> <Preview
className='full-height'
disableIframe={this.state.resizing}
previewMounted={previewMounted}
/>
); );
} }
@ -384,7 +405,8 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
const { const {
executeChallenge, executeChallenge,
pageContext: { pageContext: {
challengeMeta: { nextChallengePath, prevChallengePath } challengeMeta: { nextChallengePath, prevChallengePath },
projectPreview
}, },
challengeFiles, challengeFiles,
t t
@ -444,6 +466,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
<HelpModal /> <HelpModal />
<VideoModal videoUrl={this.getVideoUrl()} /> <VideoModal videoUrl={this.getVideoUrl()} />
<ResetModal /> <ResetModal />
<ProjectPreviewModal previewConfig={projectPreview} />
</LearnLayout> </LearnLayout>
</Hotkeys> </Hotkeys>
); );
@ -457,9 +480,6 @@ export default connect(
mapDispatchToProps mapDispatchToProps
)(withTranslation()(ShowClassic)); )(withTranslation()(ShowClassic));
// TODO: handle jsx (not sure why it doesn't get an editableRegion) EDIT:
// probably because the dummy challenge didn't include it, so Gatsby couldn't
// infer it.
export const query = graphql` export const query = graphql`
query ClassicChallenge($slug: String!) { query ClassicChallenge($slug: String!) {
challengeNode(fields: { slug: { eq: $slug } }) { challengeNode(fields: { slug: { eq: $slug } }) {

View File

@ -1,30 +1,23 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { withTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators, Dispatch } from 'redux';
import { previewMounted } from '../redux'; import { mainPreviewId } from '../utils/frame';
import './preview.css'; import './preview.css';
const mainId = 'fcc-main-frame';
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
previewMounted
},
dispatch
);
interface PreviewProps { interface PreviewProps {
className?: string; className?: string;
disableIframe?: boolean; disableIframe?: boolean;
previewMounted: () => void; previewMounted: () => void;
t: (text: string) => string; previewId?: string;
} }
function Preview({ disableIframe, previewMounted, t }: PreviewProps) { function Preview({
disableIframe,
previewMounted,
previewId
}: PreviewProps): JSX.Element {
const { t } = useTranslation();
const [iframeStatus, setIframeStatus] = useState<boolean | undefined>(false); const [iframeStatus, setIframeStatus] = useState<boolean | undefined>(false);
const iframeToggle = iframeStatus ? 'disable' : 'enable'; const iframeToggle = iframeStatus ? 'disable' : 'enable';
@ -36,11 +29,14 @@ function Preview({ disableIframe, previewMounted, t }: PreviewProps) {
setIframeStatus(disableIframe); setIframeStatus(disableIframe);
}, [disableIframe]); }, [disableIframe]);
// TODO: remove type assertion once frame.js has been migrated.
const id: string = previewId ?? (mainPreviewId as string);
return ( return (
<div className={`notranslate challenge-preview ${iframeToggle}-iframe`}> <div className={`notranslate challenge-preview ${iframeToggle}-iframe`}>
<iframe <iframe
className={'challenge-preview-frame'} className={'challenge-preview-frame'}
id={mainId} id={id}
title={t('learn.chal-preview')} title={t('learn.chal-preview')}
/> />
</div> </div>
@ -49,4 +45,4 @@ function Preview({ disableIframe, previewMounted, t }: PreviewProps) {
Preview.displayName = 'Preview'; Preview.displayName = 'Preview';
export default connect(null, mapDispatchToProps)(withTranslation()(Preview)); export default Preview;

View File

@ -0,0 +1,98 @@
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import type { ChallengeFile, Required } from '../../../redux/prop-types';
import {
closeModal,
setEditorFocusability,
isProjectPreviewModalOpenSelector,
projectPreviewMounted
} from '../redux';
import { projectPreviewId } from '../utils/frame';
import Preview from './preview';
export interface PreviewConfig {
challengeType: boolean;
challengeFiles: ChallengeFile[];
required: Required;
template: string;
}
interface Props {
closeModal: () => void;
isOpen: boolean;
projectPreviewMounted: (previewConfig: PreviewConfig) => void;
previewConfig: PreviewConfig;
setEditorFocusability: (focusability: boolean) => void;
}
const mapStateToProps = (state: unknown) => ({
isOpen: isProjectPreviewModalOpenSelector(state) as boolean
});
const mapDispatchToProps = {
closeModal: () => closeModal('projectPreview'),
setEditorFocusability,
projectPreviewMounted
};
export function ProjectPreviewModal({
closeModal,
isOpen,
projectPreviewMounted,
previewConfig,
setEditorFocusability
}: Props): JSX.Element {
const { t } = useTranslation();
useEffect(() => {
if (isOpen) setEditorFocusability(false);
});
return (
<Modal
dialogClassName='project-preview-modal'
onHide={() => {
closeModal();
setEditorFocusability(true);
}}
show={isOpen}
>
<Modal.Header
className='project-preview-modal-header fcc-modal'
closeButton={true}
>
<Modal.Title className='text-center'>
{t('learn.project-preview-title')}
</Modal.Title>
</Modal.Header>
<Modal.Body className='project-preview-modal-body text-center'>
{/* remove type assertion once frame.js has been migrated to TS */}
<Preview
previewId={projectPreviewId as string}
previewMounted={() => {
projectPreviewMounted(previewConfig);
}}
/>
<Button
block={true}
bsSize='lg'
bsStyle='primary'
onClick={() => {
closeModal();
setEditorFocusability(true);
}}
>
{t('buttons.hide-project-preview')}
</Button>
</Modal.Body>
</Modal>
);
}
ProjectPreviewModal.displayName = 'ProjectPreviewModal';
export default connect(
mapStateToProps,
mapDispatchToProps
)(ProjectPreviewModal);

View File

@ -3,11 +3,11 @@ import { format } from '../../../utils/format';
// we use two different frames to make them all essentially pure functions // we use two different frames to make them all essentially pure functions
// main iframe is responsible rendering the preview and is where we proxy the // main iframe is responsible rendering the preview and is where we proxy the
const mainPreviewId = 'fcc-main-frame'; export const mainPreviewId = 'fcc-main-frame';
// the test frame is responsible for running the assert tests // the test frame is responsible for running the assert tests
const testId = 'fcc-test-frame'; const testId = 'fcc-test-frame';
// the project preview frame demos the finished project // the project preview frame demos the finished project
const projectPreviewId = 'fcc-project-preview-frame'; export const projectPreviewId = 'fcc-project-preview-frame';
// base tag here will force relative links // base tag here will force relative links
// within iframe to point to '' instead of // within iframe to point to '' instead of