feat: show project preview for first challenge
This commit is contained in:
committed by
moT01
parent
cebda66b04
commit
943aa975d3
@ -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",
|
||||||
|
@ -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;
|
||||||
|
@ -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 } }) {
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
@ -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
|
||||||
|
Reference in New Issue
Block a user