feat(client): ts-migrate challenge templates/show components (#42553)

This commit is contained in:
Tom
2021-06-25 09:59:33 -05:00
committed by Mrugesh Mohapatra
parent 40323aef6a
commit 918d5a160d
10 changed files with 343 additions and 227 deletions

View File

@ -160,7 +160,9 @@ export type ChallengeNodeType = {
fields: {
slug: string;
blockName: string;
tests: TestType[];
};
files: ChallengeFilesType;
forumTopicId: number;
guideUrl: string;
head: string[];
@ -171,6 +173,11 @@ export type ChallengeNodeType = {
isLocked: boolean;
isPrivate: boolean;
order: number;
question: {
text: string;
answers: string[];
solution: number;
};
required: [
{
link: string;
@ -184,6 +191,8 @@ export type ChallengeNodeType = {
time: string;
title: string;
translationPending: boolean;
url: string;
videoId: string;
videoUrl: string;
};
@ -204,7 +213,7 @@ export type AllMarkdownRemarkType = {
};
export type ResizePropsType = {
onStopResize: () => void;
onStopResize: (arg0: React.ChangeEvent) => void;
onResize: () => void;
};
@ -289,6 +298,28 @@ export type ChallengeFileType = {
export type ExtTypes = 'js' | 'html' | 'css' | 'jsx';
export type FileKeyTypes = 'indexjs' | 'indexhtml' | 'indexcss';
export type ChallengeFilesType =
| {
indexcss: ChallengeFileType;
indexhtml: ChallengeFileType;
indexjs: ChallengeFileType;
indexjsx: ChallengeFileType;
}
| Record<string, never>;
export type ChallengeMetaType = {
block: string;
id: string;
introPath: string;
nextChallengePath: string;
prevChallengePath: string;
removeComments: boolean;
superBlock: string;
title?: string;
challengeType?: number;
helpCategory: string;
};
export type PortfolioType = {
id: string;
title?: string;
@ -297,6 +328,7 @@ export type PortfolioType = {
description?: string;
};
// This looks redundant - same as ChallengeNodeType above?
export type ChallengeNode = {
block: string;
challengeOrder: number;

View File

@ -1,6 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
// Package Utilities
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch } from 'redux';
import { createStructuredSelector } from 'reselect';
import { connect } from 'react-redux';
import Helmet from 'react-helmet';
@ -8,6 +11,7 @@ import { graphql } from 'gatsby';
import Media from 'react-responsive';
import { withTranslation } from 'react-i18next';
// Local Utilities
import LearnLayout from '../../../components/layouts/Learn';
import MultifileEditor from './MultifileEditor';
import Preview from '../components/Preview';
@ -20,12 +24,17 @@ import ResetModal from '../components/ResetModal';
import MobileLayout from './MobileLayout';
import DesktopLayout from './DesktopLayout';
import Hotkeys from '../components/Hotkeys';
import { getGuideUrl } from '../utils';
import store from 'store';
import { challengeTypes } from '../../../../utils/challengeTypes';
import { isContained } from '../../../utils/is-contained';
import { ChallengeNode } from '../../../redux/prop-types';
import {
ChallengeNodeType,
ChallengeFilesType,
ChallengeMetaType,
TestType,
ResizePropsType
} from '../../../redux/prop-types';
import {
createFiles,
challengeFilesSelector,
@ -39,16 +48,18 @@ import {
cancelTests
} from '../redux';
// Styles
import './classic.css';
import '../components/test-frame.css';
// Redux Setup
const mapStateToProps = createStructuredSelector({
files: challengeFilesSelector,
tests: challengeTestsSelector,
output: consoleOutputSelector
});
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
createFiles,
@ -62,60 +73,58 @@ const mapDispatchToProps = dispatch =>
dispatch
);
const propTypes = {
cancelTests: PropTypes.func.isRequired,
challengeMounted: PropTypes.func.isRequired,
createFiles: PropTypes.func.isRequired,
data: PropTypes.shape({
challengeNode: ChallengeNode
}),
executeChallenge: PropTypes.func.isRequired,
files: PropTypes.shape({
key: PropTypes.string
}),
initConsole: PropTypes.func.isRequired,
initTests: PropTypes.func.isRequired,
output: PropTypes.arrayOf(PropTypes.string),
pageContext: PropTypes.shape({
challengeMeta: PropTypes.shape({
id: PropTypes.string,
nextChallengePath: PropTypes.string,
prevChallengePath: PropTypes.string
})
}),
t: PropTypes.func.isRequired,
tests: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string,
testString: PropTypes.string
})
),
updateChallengeMeta: PropTypes.func.isRequired
};
// Types
interface ShowClassicProps {
cancelTests: () => void;
challengeMounted: (arg0: string) => void;
createFiles: (arg0: ChallengeFilesType) => void;
data: { challengeNode: ChallengeNodeType };
executeChallenge: () => void;
files: ChallengeFilesType;
initConsole: (arg0: string) => void;
initTests: (tests: TestType[]) => void;
output: string[];
pageContext: {
challengeMeta: ChallengeMetaType;
};
t: (arg0: string) => string;
tests: TestType[];
updateChallengeMeta: (arg0: ChallengeMetaType) => void;
}
interface ShowClassicState {
resizing: boolean;
}
interface IReflexLayout {
codePane: { flex: number };
editorPane: { flex: number };
instructionPane: { flex: number };
previewPane: { flex: number };
testsPane: { flex: number };
}
const MAX_MOBILE_WIDTH = 767;
const REFLEX_LAYOUT = 'challenge-layout';
const BASE_LAYOUT = {
codePane: {
flex: 1
},
editorPane: {
flex: 1
},
instructionPane: {
flex: 1
},
previewPane: {
flex: 0.7
},
testsPane: {
flex: 0.25
}
codePane: { flex: 1 },
editorPane: { flex: 1 },
instructionPane: { flex: 1 },
previewPane: { flex: 0.7 },
testsPane: { flex: 0.25 }
};
class ShowClassic extends Component {
constructor() {
super();
// Component
class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
static displayName: string;
containerRef: React.RefObject<unknown>;
editorRef: React.RefObject<unknown>;
resizeProps: ResizePropsType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
layoutState: any;
constructor(props: ShowClassicProps) {
super(props);
this.resizeProps = {
onStopResize: this.onStopResize.bind(this),
@ -132,8 +141,8 @@ class ShowClassic extends Component {
this.layoutState = this.getLayoutState();
}
getLayoutState() {
const reflexLayout = store.get(REFLEX_LAYOUT);
getLayoutState(): IReflexLayout | string {
const reflexLayout: IReflexLayout | string = store.get(REFLEX_LAYOUT);
// Validate if user has not done any resize of the panes
if (!reflexLayout) return BASE_LAYOUT;
@ -153,7 +162,8 @@ class ShowClassic extends Component {
this.setState({ resizing: true });
}
onStopResize(event) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onStopResize(event: any) {
const { name, flex } = event.component.props;
this.setState({ resizing: false });
@ -177,7 +187,7 @@ class ShowClassic extends Component {
this.initializeComponent(title);
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: ShowClassicProps) {
const {
data: {
challengeNode: {
@ -199,7 +209,7 @@ class ShowClassic extends Component {
}
}
initializeComponent(title) {
initializeComponent(title: string) {
const {
challengeMounted,
createFiles,
@ -256,7 +266,7 @@ class ShowClassic extends Component {
);
}
renderInstructionsPanel({ showToolPanel }) {
renderInstructionsPanel({ showToolPanel }: { showToolPanel: boolean }) {
const { block, description, instructions, superBlock, translationPending } =
this.getChallenge();
@ -397,7 +407,6 @@ class ShowClassic extends Component {
}
ShowClassic.displayName = 'ShowClassic';
ShowClassic.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -2,20 +2,23 @@
// Package Utilities
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Helmet from 'react-helmet';
import { graphql } from 'gatsby';
import type { Dispatch } from 'redux';
// Local Utilities
import LearnLayout from '../../../components/layouts/Learn';
import { ChallengeNode } from '../../../redux/prop-types';
import {
ChallengeNodeType,
ChallengeMetaType
} from '../../../redux/prop-types';
import { updateChallengeMeta, challengeMounted } from '../redux';
// Redux
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
updateChallengeMeta,
@ -24,25 +27,19 @@ const mapDispatchToProps = dispatch =>
dispatch
);
// Proptypes
const propTypes = {
data: PropTypes.shape({
challengeNode: ChallengeNode
}),
pageContext: PropTypes.shape({
challengeMeta: PropTypes.shape({
id: PropTypes.string,
introPath: PropTypes.string,
nextChallengePath: PropTypes.string,
prevChallengePath: PropTypes.string
})
}),
updateChallengeMeta: PropTypes.func.isRequired
};
// Types
interface ShowCodeAllyProps {
data: { challengeNode: ChallengeNodeType };
pageContext: {
challengeMeta: ChallengeMetaType;
};
updateChallengeMeta: (arg0: ChallengeMetaType) => void;
}
// Component
class ShowCodeAlly extends Component {
componentDidMount() {
class ShowCodeAlly extends Component<ShowCodeAllyProps> {
static displayName: string;
componentDidMount(): void {
const {
updateChallengeMeta,
data: {
@ -65,6 +62,7 @@ class ShowCodeAlly extends Component {
<Helmet title={`${blockName}: ${title} | freeCodeCamp.org`} />
<iframe
sandbox='allow-modals allow-forms allow-popups allow-scripts allow-same-origin'
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
src={`http://codeally.io/embed/?repoUrl=${url}`}
style={{
width: '100%',
@ -80,7 +78,6 @@ class ShowCodeAlly extends Component {
}
ShowCodeAlly.displayName = 'ShowCodeAlly';
ShowCodeAlly.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(ShowCodeAlly);

View File

@ -4,9 +4,9 @@ import PrismFormatted from './PrismFormatted';
import './challenge-description.css';
type Challenge = {
block: string;
description: string;
instructions: string;
block?: string;
description?: string;
instructions?: string;
};
function ChallengeDescription(challenge: Challenge): JSX.Element {

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import React from 'react';
import PropTypes from 'prop-types';
import { HotKeys, GlobalHotKeys } from 'react-hotkeys';
import { navigate } from 'gatsby';
import { connect } from 'react-redux';
@ -10,7 +11,7 @@ import './hotkeys.css';
const mapStateToProps = createSelector(
canFocusEditorSelector,
canFocusEditor => ({
(canFocusEditor: boolean) => ({
canFocusEditor
})
);
@ -25,16 +26,17 @@ const keyMap = {
NAVIGATE_NEXT: ['n']
};
const propTypes = {
canFocusEditor: PropTypes.bool,
children: PropTypes.any,
editorRef: PropTypes.object,
executeChallenge: PropTypes.func,
innerRef: PropTypes.any,
nextChallengePath: PropTypes.string,
prevChallengePath: PropTypes.string,
setEditorFocusability: PropTypes.func.isRequired
};
interface HotkeysProps {
canFocusEditor: boolean;
children: React.ReactElement;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
editorRef?: React.Ref<HTMLElement> | any;
executeChallenge?: () => void;
innerRef: React.Ref<HTMLElement> | unknown;
nextChallengePath: string;
prevChallengePath: string;
setEditorFocusability: (arg0: boolean) => void;
}
function Hotkeys({
canFocusEditor,
@ -45,9 +47,9 @@ function Hotkeys({
nextChallengePath,
prevChallengePath,
setEditorFocusability
}) {
}: HotkeysProps): JSX.Element {
const handlers = {
EXECUTE_CHALLENGE: e => {
EXECUTE_CHALLENGE: (e: React.KeyboardEvent<HTMLButtonElement>) => {
// the 'enter' part of 'ctrl+enter' stops HotKeys from listening, so it
// needs to be prevented.
// TODO: 'enter' on its own also disables HotKeys, but default behaviour
@ -55,7 +57,7 @@ function Hotkeys({
e.preventDefault();
if (executeChallenge) executeChallenge();
},
FOCUS_EDITOR: e => {
FOCUS_EDITOR: (e: React.KeyboardEvent) => {
e.preventDefault();
if (editorRef && editorRef.current) {
editorRef.current.getWrappedInstance().focusOnEditor();
@ -63,10 +65,10 @@ function Hotkeys({
},
NAVIGATION_MODE: () => setEditorFocusability(false),
NAVIGATE_PREV: () => {
if (!canFocusEditor) navigate(prevChallengePath);
if (!canFocusEditor) void navigate(prevChallengePath);
},
NAVIGATE_NEXT: () => {
if (!canFocusEditor) navigate(nextChallengePath);
if (!canFocusEditor) void navigate(nextChallengePath);
}
};
// GlobalHotKeys is always mounted and tracks all keypresses. Without it,
@ -75,19 +77,22 @@ function Hotkeys({
// allowChanges is necessary if the handlers depend on props (in this case
// canFocusEditor)
return (
<HotKeys
allowChanges={true}
handlers={handlers}
innerRef={innerRef}
keyMap={keyMap}
>
{children}
<GlobalHotKeys />
</HotKeys>
<>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<HotKeys
allowChanges={true}
handlers={handlers}
innerRef={innerRef}
keyMap={keyMap}
>
{children}
<GlobalHotKeys />
</HotKeys>
</>
);
}
Hotkeys.displayName = 'Hotkeys';
Hotkeys.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(Hotkeys);

View File

@ -1,28 +1,35 @@
// Package Utilities
import React from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { bindActionCreators, Dispatch } from 'redux';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button, Modal } from '@freecodecamp/react-bootstrap';
import { useTranslation } from 'react-i18next';
// Local Utilities
import { isResetModalOpenSelector, closeModal, resetChallenge } from '../redux';
import { executeGA } from '../../../redux';
// Styles
import './reset-modal.css';
const propTypes = {
close: PropTypes.func.isRequired,
executeGA: PropTypes.func,
isOpen: PropTypes.bool.isRequired,
reset: PropTypes.func.isRequired
};
// Types
interface ResetModalProps {
close: () => void;
executeGA: () => void;
isOpen: boolean;
reset: () => void;
}
const mapStateToProps = createSelector(isResetModalOpenSelector, isOpen => ({
isOpen
}));
// Redux Setup
const mapStateToProps = createSelector(
isResetModalOpenSelector,
(isOpen: boolean) => ({
isOpen
})
);
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
close: () => closeModal('reset'),
@ -32,11 +39,12 @@ const mapDispatchToProps = dispatch =>
dispatch
);
function withActions(...fns) {
function withActions(...fns: Array<() => void>) {
return () => fns.forEach(fn => fn());
}
function ResetModal({ reset, close, isOpen }) {
// Component
function ResetModal({ reset, close, isOpen }: ResetModalProps): JSX.Element {
const { t } = useTranslation();
if (isOpen) {
executeGA({ type: 'modal', data: '/reset-modal' });
@ -75,6 +83,5 @@ function ResetModal({ reset, close, isOpen }) {
}
ResetModal.displayName = 'ResetModal';
ResetModal.propTypes = propTypes;
export default connect(mapStateToProps, mapDispatchToProps)(ResetModal);

View File

@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
// Package Utilities
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Grid, Col, Row } from '@freecodecamp/react-bootstrap';
import { createSelector } from 'reselect';
import { connect } from 'react-redux';
@ -7,6 +9,7 @@ import { graphql } from 'gatsby';
import Helmet from 'react-helmet';
import { withTranslation } from 'react-i18next';
// Local Utilities
import {
executeChallenge,
challengeMounted,
@ -19,7 +22,6 @@ import {
updateSolutionFormValues
} from '../../redux';
import { getGuideUrl } from '../../utils';
import LearnLayout from '../../../../components/layouts/Learn';
import ChallengeTitle from '../../components/Challenge-Title';
import ChallengeDescription from '../../components/Challenge-Description';
@ -30,42 +32,29 @@ import HelpModal from '../../components/HelpModal';
import ProjectToolPanel from '../Tool-Panel';
import SolutionForm from '../SolutionForm';
import Spacer from '../../../../components/helpers/spacer';
import { ChallengeNode } from '../../../../redux/prop-types';
import {
ChallengeNodeType,
ChallengeMetaType,
TestType
} from '../../../../redux/prop-types';
import { isSignedInSelector } from '../../../../redux';
import Hotkeys from '../../components/Hotkeys';
// Styles
import '../../components/test-frame.css';
const propTypes = {
challengeMounted: PropTypes.func.isRequired,
data: PropTypes.shape({
challengeNode: ChallengeNode
}),
description: PropTypes.string,
executeChallenge: PropTypes.func.isRequired,
forumTopicId: PropTypes.number,
id: PropTypes.string,
initConsole: PropTypes.func.isRequired,
initTests: PropTypes.func.isRequired,
isChallengeCompleted: PropTypes.bool,
isSignedIn: PropTypes.bool,
output: PropTypes.arrayOf(PropTypes.string),
pageContext: PropTypes.shape({
challengeMeta: PropTypes.object
}),
t: PropTypes.func.isRequired,
tests: PropTypes.array,
title: PropTypes.string,
updateChallengeMeta: PropTypes.func.isRequired,
updateSolutionFormValues: PropTypes.func.isRequired
};
// Redux Setup
const mapStateToProps = createSelector(
consoleOutputSelector,
challengeTestsSelector,
isChallengeCompletedSelector,
isSignedInSelector,
(output, tests, isChallengeCompleted, isSignedIn) => ({
(
output: string[],
tests: TestType[],
isChallengeCompleted: boolean,
isSignedIn: boolean
) => ({
tests,
output,
isChallengeCompleted,
@ -82,8 +71,35 @@ const mapDispatchToActions = {
updateSolutionFormValues
};
class BackEnd extends Component {
constructor(props) {
// Types
interface BackEndProps {
challengeMounted: (arg0: string) => void;
data: { challengeNode: ChallengeNodeType };
description: string;
executeChallenge: (arg0: boolean) => void;
forumTopicId: number;
id: string;
initConsole: () => void;
initTests: (tests: TestType[]) => void;
isChallengeCompleted: boolean;
isSignedIn: boolean;
output: string[];
pageContext: {
challengeMeta: ChallengeMetaType;
};
t: (arg0: string) => string;
tests: TestType[];
title: string;
updateChallengeMeta: (arg0: ChallengeMetaType) => void;
updateSolutionFormValues: () => void;
}
// Component
class BackEnd extends Component<BackEndProps> {
static displayName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _container: any;
constructor(props: BackEndProps) {
super(props);
this.state = {};
this.updateDimensions = this.updateDimensions.bind(this);
@ -92,7 +108,7 @@ class BackEnd extends Component {
componentDidMount() {
this.initializeComponent();
window.addEventListener('resize', this.updateDimensions);
window.addEventListener('resize', () => this.updateDimensions());
this._container.focus();
}
@ -101,10 +117,10 @@ class BackEnd extends Component {
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateDimensions);
window.removeEventListener('resize', () => this.updateDimensions());
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: BackEndProps) {
const {
data: {
challengeNode: {
@ -122,7 +138,7 @@ class BackEnd extends Component {
}
} = this.props;
if (prevTitle !== currentTitle || prevTests !== currTests) {
this.initializeComponent(currentTitle);
this.initializeComponent();
}
}
@ -153,7 +169,11 @@ class BackEnd extends Component {
challengeMounted(challengeMeta.id);
}
handleSubmit({ isShouldCompletionModalOpen }) {
handleSubmit({
isShouldCompletionModalOpen
}: {
isShouldCompletionModalOpen: boolean;
}): void {
this.props.executeChallenge(isShouldCompletionModalOpen);
}
@ -186,7 +206,7 @@ class BackEnd extends Component {
return (
<Hotkeys
innerRef={c => (this._container = c)}
innerRef={(c: HTMLElement | null) => (this._container = c)}
nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath}
>
@ -197,6 +217,8 @@ class BackEnd extends Component {
<Grid>
<Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
<ChallengeTitle
block={block}
@ -212,6 +234,7 @@ class BackEnd extends Component {
/>
<SolutionForm
challengeType={challengeType}
// eslint-disable-next-line @typescript-eslint/unbound-method
onSubmit={this.handleSubmit}
updateSolutionForm={updateSolutionFormValues}
/>
@ -231,6 +254,8 @@ class BackEnd extends Component {
output={output}
/>
<TestSuite tests={tests} />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
</Col>
<CompletionModal
@ -248,7 +273,6 @@ class BackEnd extends Component {
}
BackEnd.displayName = 'BackEnd';
BackEnd.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -1,5 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
// Package Utilities
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Grid, Col, Row } from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
@ -7,8 +9,13 @@ import { graphql } from 'gatsby';
import Helmet from 'react-helmet';
import { withTranslation } from 'react-i18next';
import { createSelector } from 'reselect';
import type { Dispatch } from 'redux';
import { ChallengeNode } from '../../../../redux/prop-types';
// Local Utilities
import {
ChallengeNodeType,
ChallengeMetaType
} from '../../../../redux/prop-types';
import {
challengeMounted,
isChallengeCompletedSelector,
@ -16,9 +23,7 @@ import {
openModal,
updateSolutionFormValues
} from '../../redux';
import { getGuideUrl } from '../../utils';
import LearnLayout from '../../../../components/layouts/Learn';
import ChallengeTitle from '../../components/Challenge-Title';
import ChallengeDescription from '../../components/Challenge-Description';
@ -29,14 +34,15 @@ import CompletionModal from '../../components/CompletionModal';
import HelpModal from '../../components/HelpModal';
import Hotkeys from '../../components/Hotkeys';
// Redux Setup
const mapStateToProps = createSelector(
isChallengeCompletedSelector,
isChallengeCompleted => ({
(isChallengeCompleted: boolean) => ({
isChallengeCompleted
})
);
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
updateChallengeMeta,
@ -47,24 +53,28 @@ const mapDispatchToProps = dispatch =>
dispatch
);
const propTypes = {
challengeMounted: PropTypes.func.isRequired,
data: PropTypes.shape({
challengeNode: ChallengeNode
}),
isChallengeCompleted: PropTypes.bool,
openCompletionModal: PropTypes.func.isRequired,
pageContext: PropTypes.shape({
challengeMeta: PropTypes.object
}),
t: PropTypes.func.isRequired,
updateChallengeMeta: PropTypes.func.isRequired,
updateSolutionFormValues: PropTypes.func.isRequired
};
// Types
interface ProjectProps {
challengeMounted: (arg0: string) => void;
data: { challengeNode: ChallengeNodeType };
isChallengeCompleted: boolean;
openCompletionModal: () => void;
pageContext: {
challengeMeta: ChallengeMetaType;
};
t: (arg0: string) => string;
updateChallengeMeta: (arg0: ChallengeMetaType) => void;
updateSolutionFormValues: () => void;
}
class Project extends Component {
constructor() {
super();
// Component
class Project extends Component<ProjectProps> {
static displayName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _container: any;
constructor(props: ProjectProps) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
@ -86,7 +96,7 @@ class Project extends Component {
this._container.focus();
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: ProjectProps): void {
const {
data: {
challengeNode: { title: prevTitle }
@ -111,7 +121,11 @@ class Project extends Component {
}
}
handleSubmit({ isShouldCompletionModalOpen }) {
handleSubmit({
isShouldCompletionModalOpen
}: {
isShouldCompletionModalOpen: boolean;
}): void {
if (isShouldCompletionModalOpen) {
this.props.openCompletionModal();
}
@ -143,7 +157,7 @@ class Project extends Component {
return (
<Hotkeys
innerRef={c => (this._container = c)}
innerRef={(c: HTMLElement | null) => (this._container = c)}
nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath}
>
@ -154,6 +168,8 @@ class Project extends Component {
<Grid>
<Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
<ChallengeTitle
block={block}
@ -167,6 +183,7 @@ class Project extends Component {
<SolutionForm
challengeType={challengeType}
description={description}
// eslint-disable-next-line @typescript-eslint/unbound-method
onSubmit={this.handleSubmit}
updateSolutionForm={updateSolutionFormValues}
/>
@ -174,6 +191,8 @@ class Project extends Component {
guideUrl={getGuideUrl({ forumTopicId, title })}
/>
<br />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
</Col>
<CompletionModal
@ -191,7 +210,6 @@ class Project extends Component {
}
Project.displayName = 'Project';
Project.propTypes = propTypes;
export default connect(
mapStateToProps,

View File

@ -1,6 +1,5 @@
// Package Utilities
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button, Grid, Col, Row } from '@freecodecamp/react-bootstrap';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
@ -10,10 +9,14 @@ import YouTube from 'react-youtube';
import { createSelector } from 'reselect';
import { ObserveKeys } from 'react-hotkeys';
import { withTranslation } from 'react-i18next';
import type { Dispatch } from 'redux';
// Local Utilities
import PrismFormatted from '../components/PrismFormatted';
import { ChallengeNode } from '../../../redux/prop-types';
import {
ChallengeNodeType,
ChallengeMetaType
} from '../../../redux/prop-types';
import LearnLayout from '../../../components/layouts/Learn';
import ChallengeTitle from '../components/Challenge-Title';
import ChallengeDescription from '../components/Challenge-Description';
@ -35,11 +38,11 @@ import './show.css';
// Redux Setup
const mapStateToProps = createSelector(
isChallengeCompletedSelector,
isChallengeCompleted => ({
(isChallengeCompleted: boolean) => ({
isChallengeCompleted
})
);
const mapDispatchToProps = dispatch =>
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
updateChallengeMeta,
@ -50,26 +53,36 @@ const mapDispatchToProps = dispatch =>
dispatch
);
// Proptypes
const propTypes = {
challengeMounted: PropTypes.func.isRequired,
data: PropTypes.shape({
challengeNode: ChallengeNode
}),
description: PropTypes.string,
isChallengeCompleted: PropTypes.bool,
openCompletionModal: PropTypes.func.isRequired,
pageContext: PropTypes.shape({
challengeMeta: PropTypes.object
}),
t: PropTypes.func.isRequired,
updateChallengeMeta: PropTypes.func.isRequired,
updateSolutionFormValues: PropTypes.func.isRequired
};
// Types
interface ShowVideoProps {
challengeMounted: (arg0: string) => void;
data: { challengeNode: ChallengeNodeType };
description: string;
isChallengeCompleted: boolean;
openCompletionModal: () => void;
pageContext: {
challengeMeta: ChallengeMetaType;
};
t: (arg0: string) => string;
updateChallengeMeta: (arg0: ChallengeMetaType) => void;
updateSolutionFormValues: () => void;
}
interface ShowVideoState {
subtitles: string;
downloadURL: string | null;
selectedOption: number | null;
answer: number;
showWrong: boolean;
videoIsLoaded: boolean;
}
// Component
class Project extends Component {
constructor(props) {
class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
static displayName: string;
private _container: HTMLElement | null | undefined;
constructor(props: ShowVideoProps) {
super(props);
this.state = {
subtitles: '',
@ -83,7 +96,7 @@ class Project extends Component {
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
componentDidMount(): void {
const {
challengeMounted,
data: {
@ -99,10 +112,9 @@ class Project extends Component {
helpCategory
});
challengeMounted(challengeMeta.id);
this._container.focus();
}
componentDidUpdate(prevProps) {
componentDidUpdate(prevProps: ShowVideoProps): void {
const {
data: {
challengeNode: { title: prevTitle }
@ -127,7 +139,7 @@ class Project extends Component {
}
}
handleSubmit(solution, openCompletionModal) {
handleSubmit(solution: number, openCompletionModal: () => void) {
if (solution - 1 === this.state.selectedOption) {
this.setState({
showWrong: false
@ -140,7 +152,9 @@ class Project extends Component {
}
}
handleOptionChange = changeEvent => {
handleOptionChange = (
changeEvent: React.ChangeEvent<HTMLInputElement>
): void => {
this.setState({
showWrong: false,
selectedOption: parseInt(changeEvent.target.value, 10)
@ -181,7 +195,7 @@ class Project extends Component {
executeChallenge={() => {
this.handleSubmit(solution, openCompletionModal);
}}
innerRef={c => (this._container = c)}
innerRef={(c: HTMLElement | null) => (this._container = c)}
nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath}
>
@ -191,6 +205,8 @@ class Project extends Component {
/>
<Grid>
<Row>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
<ChallengeTitle
block={block}
@ -222,6 +238,7 @@ class Project extends Component {
width: 'auto',
height: 'auto'
}}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
videoId={videoId}
/>
<i>
@ -243,6 +260,8 @@ class Project extends Component {
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<ChallengeDescription description={description} />
<PrismFormatted className={'line-numbers'} text={text} />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
<ObserveKeys>
<div className='video-quiz-options'>
@ -272,6 +291,8 @@ class Project extends Component {
))}
</div>
</ObserveKeys>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
<div
style={{
@ -284,6 +305,8 @@ class Project extends Component {
<span>{t('learn.check-answer')}</span>
)}
</div>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer />
<Button
block={true}
@ -295,6 +318,8 @@ class Project extends Component {
>
{t('buttons.check-answer')}
</Button>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<Spacer size={2} />
</Col>
<CompletionModal
@ -310,13 +335,12 @@ class Project extends Component {
}
}
Project.displayName = 'Project';
Project.propTypes = propTypes;
ShowVideo.displayName = 'ShowVideo';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(Project));
)(withTranslation()(ShowVideo));
export const query = graphql`
query VideoChallenge($slug: String!) {

View File

@ -5,19 +5,19 @@ const { viewTypes } = require('../challengeTypes');
const backend = path.resolve(
__dirname,
'../../src/templates/Challenges/projects/backend/Show.js'
'../../src/templates/Challenges/projects/backend/Show.tsx'
);
const classic = path.resolve(
__dirname,
'../../src/templates/Challenges/classic/Show.js'
'../../src/templates/Challenges/classic/Show.tsx'
);
const frontend = path.resolve(
__dirname,
'../../src/templates/Challenges/projects/frontend/Show.js'
'../../src/templates/Challenges/projects/frontend/Show.tsx'
);
const codeally = path.resolve(
__dirname,
'../../src/templates/Challenges/codeally/show.js'
'../../src/templates/Challenges/codeally/show.tsx'
);
const intro = path.resolve(
__dirname,
@ -29,7 +29,7 @@ const superBlockIntro = path.resolve(
);
const video = path.resolve(
__dirname,
'../../src/templates/Challenges/video/Show.js'
'../../src/templates/Challenges/video/Show.tsx'
);
const views = {