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",
|
||||
"submit-and-go": "Submit and 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": {
|
||||
"big-heading-1": "Learn to code — for free.",
|
||||
@ -288,7 +289,8 @@
|
||||
"preview": "Preview"
|
||||
},
|
||||
"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": {
|
||||
"title": "Support our nonprofit",
|
||||
|
@ -105,7 +105,7 @@ export type MarkdownRemark = {
|
||||
|
||||
type Question = { text: string; answers: string[]; solution: number };
|
||||
type Fields = { slug: string; blockName: string; tests: Test[] };
|
||||
type Required = {
|
||||
export type Required = {
|
||||
link: string;
|
||||
raw: boolean;
|
||||
src: string;
|
||||
|
@ -28,6 +28,9 @@ import CompletionModal from '../components/completion-modal';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import Output from '../components/output';
|
||||
import Preview from '../components/preview';
|
||||
import ProjectPreviewModal, {
|
||||
PreviewConfig
|
||||
} from '../components/project-preview-modal';
|
||||
import SidePanel from '../components/side-panel';
|
||||
import VideoModal from '../components/video-modal';
|
||||
import {
|
||||
@ -41,7 +44,10 @@ import {
|
||||
initConsole,
|
||||
initTests,
|
||||
isChallengeCompletedSelector,
|
||||
updateChallengeMeta
|
||||
previewMounted,
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
setEditorFocusability
|
||||
} from '../redux';
|
||||
import { getGuideUrl } from '../utils';
|
||||
import MultifileEditor from './MultifileEditor';
|
||||
@ -68,7 +74,10 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
updateChallengeMeta,
|
||||
challengeMounted,
|
||||
executeChallenge,
|
||||
cancelTests
|
||||
cancelTests,
|
||||
previewMounted,
|
||||
openModal,
|
||||
setEditorFocusability
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
@ -87,10 +96,13 @@ interface ShowClassicProps {
|
||||
output: string[];
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMeta;
|
||||
projectPreview: PreviewConfig & { isFirstChallengeInBlock: boolean };
|
||||
};
|
||||
t: TFunction;
|
||||
tests: Test[];
|
||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||
openModal: (modal: string) => void;
|
||||
setEditorFocusability: (canFocus: boolean) => void;
|
||||
}
|
||||
|
||||
interface ShowClassicState {
|
||||
@ -231,6 +243,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
initConsole,
|
||||
initTests,
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challengeFiles,
|
||||
@ -240,11 +253,15 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
helpCategory
|
||||
}
|
||||
},
|
||||
pageContext: { challengeMeta }
|
||||
pageContext: {
|
||||
challengeMeta,
|
||||
projectPreview: { isFirstChallengeInBlock }
|
||||
}
|
||||
} = this.props;
|
||||
initConsole('');
|
||||
createFiles(challengeFiles ?? []);
|
||||
initTests(tests);
|
||||
if (isFirstChallengeInBlock) openModal('projectPreview');
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
title,
|
||||
@ -359,7 +376,11 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
|
||||
renderPreview() {
|
||||
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 {
|
||||
executeChallenge,
|
||||
pageContext: {
|
||||
challengeMeta: { nextChallengePath, prevChallengePath }
|
||||
challengeMeta: { nextChallengePath, prevChallengePath },
|
||||
projectPreview
|
||||
},
|
||||
challengeFiles,
|
||||
t
|
||||
@ -444,6 +466,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
<HelpModal />
|
||||
<VideoModal videoUrl={this.getVideoUrl()} />
|
||||
<ResetModal />
|
||||
<ProjectPreviewModal previewConfig={projectPreview} />
|
||||
</LearnLayout>
|
||||
</Hotkeys>
|
||||
);
|
||||
@ -457,9 +480,6 @@ export default connect(
|
||||
mapDispatchToProps
|
||||
)(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`
|
||||
query ClassicChallenge($slug: String!) {
|
||||
challengeNode(fields: { slug: { eq: $slug } }) {
|
||||
|
@ -1,30 +1,23 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { previewMounted } from '../redux';
|
||||
import { mainPreviewId } from '../utils/frame';
|
||||
|
||||
import './preview.css';
|
||||
|
||||
const mainId = 'fcc-main-frame';
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
previewMounted
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
interface PreviewProps {
|
||||
className?: string;
|
||||
disableIframe?: boolean;
|
||||
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 iframeToggle = iframeStatus ? 'disable' : 'enable';
|
||||
|
||||
@ -36,11 +29,14 @@ function Preview({ disableIframe, previewMounted, t }: PreviewProps) {
|
||||
setIframeStatus(disableIframe);
|
||||
}, [disableIframe]);
|
||||
|
||||
// TODO: remove type assertion once frame.js has been migrated.
|
||||
const id: string = previewId ?? (mainPreviewId as string);
|
||||
|
||||
return (
|
||||
<div className={`notranslate challenge-preview ${iframeToggle}-iframe`}>
|
||||
<iframe
|
||||
className={'challenge-preview-frame'}
|
||||
id={mainId}
|
||||
id={id}
|
||||
title={t('learn.chal-preview')}
|
||||
/>
|
||||
</div>
|
||||
@ -49,4 +45,4 @@ function Preview({ disableIframe, previewMounted, t }: PreviewProps) {
|
||||
|
||||
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
|
||||
// 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
|
||||
const testId = 'fcc-test-frame';
|
||||
// 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
|
||||
// within iframe to point to '' instead of
|
||||
|
Reference in New Issue
Block a user