mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-30 03:03:06 -05:00
refactor(client): use generic component for dialogues (#56752)
This commit is contained in:
committed by
GitHub
parent
5a7855039b
commit
ff7e0cf682
@@ -1,370 +0,0 @@
|
||||
// Package Utilities
|
||||
import { graphql } from 'gatsby';
|
||||
import React, { Component } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { ObserveKeys } from 'react-hotkeys';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { 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, Button } from '@freecodecamp/ui';
|
||||
import ShortcutsModal from '../components/shortcuts-modal';
|
||||
|
||||
// Local Utilities
|
||||
import Spacer from '../../../components/helpers/spacer';
|
||||
import LearnLayout from '../../../components/layouts/learn';
|
||||
import { ChallengeNode, ChallengeMeta, Test } from '../../../redux/prop-types';
|
||||
import Hotkeys from '../components/hotkeys';
|
||||
import CompletionModal from '../components/completion-modal';
|
||||
import ChallengeTitle from '../components/challenge-title';
|
||||
import HelpModal from '../components/help-modal';
|
||||
import PrismFormatted from '../components/prism-formatted';
|
||||
import {
|
||||
challengeMounted,
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
initTests
|
||||
} from '../redux/actions';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
import Scene from '../components/scene/scene';
|
||||
import Assignments from '../components/assignments';
|
||||
|
||||
// Styles
|
||||
import '../odin/show.css';
|
||||
import '../video.css';
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = createSelector(
|
||||
isChallengeCompletedSelector,
|
||||
(isChallengeCompleted: boolean) => ({
|
||||
isChallengeCompleted
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
initTests,
|
||||
updateChallengeMeta,
|
||||
challengeMounted,
|
||||
openCompletionModal: () => openModal('completion'),
|
||||
openHelpModal: () => openModal('help')
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
// Types
|
||||
interface ShowDialogueProps {
|
||||
challengeMounted: (arg0: string) => void;
|
||||
data: { challengeNode: ChallengeNode };
|
||||
description: string;
|
||||
initTests: (xs: Test[]) => void;
|
||||
isChallengeCompleted: boolean;
|
||||
openCompletionModal: () => void;
|
||||
openHelpModal: () => void;
|
||||
pageContext: {
|
||||
challengeMeta: ChallengeMeta;
|
||||
};
|
||||
t: TFunction;
|
||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||
}
|
||||
|
||||
interface ShowDialogueState {
|
||||
subtitles: string;
|
||||
downloadURL: string | null;
|
||||
assignmentsCompleted: number;
|
||||
allAssignmentsCompleted: boolean;
|
||||
videoIsLoaded: boolean;
|
||||
isScenePlaying: boolean;
|
||||
}
|
||||
|
||||
// Component
|
||||
class ShowDialogue extends Component<ShowDialogueProps, ShowDialogueState> {
|
||||
static displayName: string;
|
||||
private container: React.RefObject<HTMLElement> = React.createRef();
|
||||
|
||||
constructor(props: ShowDialogueProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isScenePlaying: false,
|
||||
subtitles: '',
|
||||
downloadURL: null,
|
||||
assignmentsCompleted: 0,
|
||||
allAssignmentsCompleted: false,
|
||||
videoIsLoaded: false
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
const {
|
||||
challengeMounted,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
fields: { tests },
|
||||
title,
|
||||
challengeType,
|
||||
helpCategory
|
||||
}
|
||||
}
|
||||
},
|
||||
pageContext: { challengeMeta },
|
||||
initTests,
|
||||
updateChallengeMeta
|
||||
} = this.props;
|
||||
initTests(tests);
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
title,
|
||||
challengeType,
|
||||
helpCategory
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
this.container.current?.focus();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: ShowDialogueProps): void {
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { title: prevTitle }
|
||||
}
|
||||
}
|
||||
} = prevProps;
|
||||
const {
|
||||
challengeMounted,
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: { title: currentTitle, challengeType, helpCategory }
|
||||
}
|
||||
},
|
||||
pageContext: { challengeMeta },
|
||||
updateChallengeMeta
|
||||
} = this.props;
|
||||
if (prevTitle !== currentTitle) {
|
||||
updateChallengeMeta({
|
||||
...challengeMeta,
|
||||
title: currentTitle,
|
||||
challengeType,
|
||||
helpCategory
|
||||
});
|
||||
challengeMounted(challengeMeta.id);
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const { openCompletionModal } = this.props;
|
||||
if (this.state.allAssignmentsCompleted) {
|
||||
openCompletionModal();
|
||||
}
|
||||
}
|
||||
|
||||
handleAssignmentChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
totalAssignments: number
|
||||
): void => {
|
||||
const assignmentsCompleted = event.target.checked
|
||||
? this.state.assignmentsCompleted + 1
|
||||
: this.state.assignmentsCompleted - 1;
|
||||
const allAssignmentsCompleted = totalAssignments === assignmentsCompleted;
|
||||
|
||||
this.setState({
|
||||
assignmentsCompleted,
|
||||
allAssignmentsCompleted
|
||||
});
|
||||
};
|
||||
|
||||
onVideoLoad = () => {
|
||||
this.setState({
|
||||
videoIsLoaded: true
|
||||
});
|
||||
};
|
||||
|
||||
setIsScenePlaying = (shouldPlay: boolean) => {
|
||||
this.setState({
|
||||
isScenePlaying: shouldPlay
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
data: {
|
||||
challengeNode: {
|
||||
challenge: {
|
||||
title,
|
||||
description,
|
||||
instructions,
|
||||
superBlock,
|
||||
block,
|
||||
fields: { blockName },
|
||||
assignments,
|
||||
translationPending,
|
||||
scene
|
||||
}
|
||||
}
|
||||
},
|
||||
openHelpModal,
|
||||
pageContext: {
|
||||
challengeMeta: { nextChallengePath, prevChallengePath }
|
||||
},
|
||||
isChallengeCompleted,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
const blockNameTitle = `${t(
|
||||
`intro:${superBlock}.blocks.${block}.title`
|
||||
)} - ${title}`;
|
||||
|
||||
return (
|
||||
<Hotkeys
|
||||
executeChallenge={() => this.handleSubmit()}
|
||||
containerRef={this.container}
|
||||
nextChallengePath={nextChallengePath}
|
||||
prevChallengePath={prevChallengePath}
|
||||
playScene={() => this.setIsScenePlaying(true)}
|
||||
>
|
||||
<LearnLayout>
|
||||
<Helmet
|
||||
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
|
||||
/>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer size='medium' />
|
||||
|
||||
<ChallengeTitle
|
||||
isCompleted={isChallengeCompleted}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
</ChallengeTitle>
|
||||
<PrismFormatted className={'line-numbers'} text={description} />
|
||||
<Spacer size='medium' />
|
||||
</Col>
|
||||
|
||||
{scene && (
|
||||
<Scene
|
||||
scene={scene}
|
||||
isPlaying={this.state.isScenePlaying}
|
||||
setIsPlaying={this.setIsScenePlaying}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer size='medium' />
|
||||
{instructions && (
|
||||
<>
|
||||
<PrismFormatted
|
||||
className={'line-numbers'}
|
||||
text={instructions}
|
||||
/>
|
||||
<Spacer size='medium' />
|
||||
</>
|
||||
)}
|
||||
<ObserveKeys>
|
||||
<Assignments
|
||||
assignments={assignments}
|
||||
allAssignmentsCompleted={this.state.allAssignmentsCompleted}
|
||||
handleAssignmentChange={this.handleAssignmentChange}
|
||||
/>
|
||||
</ObserveKeys>
|
||||
<Spacer size='medium' />
|
||||
<Button
|
||||
block={true}
|
||||
variant='primary'
|
||||
disabled={!this.state.allAssignmentsCompleted}
|
||||
onClick={() => this.handleSubmit()}
|
||||
>
|
||||
{t('buttons.submit')}
|
||||
</Button>
|
||||
<Spacer size='xxSmall' />
|
||||
<Button block={true} variant='primary' onClick={openHelpModal}>
|
||||
{t('buttons.ask-for-help')}
|
||||
</Button>
|
||||
<Spacer size='large' />
|
||||
</Col>
|
||||
<CompletionModal />
|
||||
<HelpModal challengeTitle={title} challengeBlock={blockName} />
|
||||
</Row>
|
||||
</Container>
|
||||
<ShortcutsModal />
|
||||
</LearnLayout>
|
||||
</Hotkeys>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ShowDialogue.displayName = 'ShowDialogue';
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withTranslation()(ShowDialogue));
|
||||
|
||||
export const query = graphql`
|
||||
query Dialogue($id: String!) {
|
||||
challengeNode(id: { eq: $id }) {
|
||||
challenge {
|
||||
videoId
|
||||
title
|
||||
description
|
||||
instructions
|
||||
challengeType
|
||||
helpCategory
|
||||
superBlock
|
||||
block
|
||||
fields {
|
||||
slug
|
||||
blockName
|
||||
tests {
|
||||
text
|
||||
testString
|
||||
}
|
||||
}
|
||||
translationPending
|
||||
assignments
|
||||
scene {
|
||||
setup {
|
||||
background
|
||||
characters {
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
}
|
||||
audio {
|
||||
filename
|
||||
startTime
|
||||
startTimestamp
|
||||
finishTimestamp
|
||||
}
|
||||
alwaysShowDialogue
|
||||
}
|
||||
commands {
|
||||
background
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
startTime
|
||||
finishTime
|
||||
dialogue {
|
||||
text
|
||||
align
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '../redux/actions';
|
||||
import { isChallengeCompletedSelector } from '../redux/selectors';
|
||||
import { BlockTypes } from '../../../../../shared/config/blocks';
|
||||
import Scene from '../components/scene/scene';
|
||||
|
||||
// Redux Setup
|
||||
const mapStateToProps = (state: unknown) => ({
|
||||
@@ -69,6 +70,7 @@ const ShowGeneric = ({
|
||||
instructions,
|
||||
title,
|
||||
translationPending,
|
||||
scene,
|
||||
superBlock,
|
||||
videoId,
|
||||
videoLocaleIds
|
||||
@@ -127,6 +129,9 @@ const ShowGeneric = ({
|
||||
setVideoIsLoaded(true);
|
||||
};
|
||||
|
||||
// scene
|
||||
const [isScenePlaying, setIsScenePlaying] = useState(false);
|
||||
|
||||
// assignments
|
||||
const [assignmentsCompleted, setAssignmentsCompleted] = useState(0);
|
||||
const allAssignmentsCompleted = assignmentsCompleted === assignments.length;
|
||||
@@ -151,6 +156,7 @@ const ShowGeneric = ({
|
||||
containerRef={container}
|
||||
nextChallengePath={nextChallengePath}
|
||||
prevChallengePath={prevChallengePath}
|
||||
playScene={scene ? () => setIsScenePlaying(true) : undefined}
|
||||
>
|
||||
<LearnLayout>
|
||||
<Helmet
|
||||
@@ -166,11 +172,12 @@ const ShowGeneric = ({
|
||||
{title}
|
||||
</ChallengeTitle>
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
{description && (
|
||||
{description && (
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<ChallengeDescription description={description} />
|
||||
)}
|
||||
</Col>
|
||||
<Spacer size='medium' />
|
||||
</Col>
|
||||
)}
|
||||
|
||||
<Col lg={10} lgOffset={1} md={10} mdOffset={1}>
|
||||
{videoId && (
|
||||
@@ -185,9 +192,17 @@ const ShowGeneric = ({
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{scene && (
|
||||
<Scene
|
||||
scene={scene}
|
||||
isPlaying={isScenePlaying}
|
||||
setIsPlaying={setIsScenePlaying}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
{instructions && (
|
||||
<ChallengeDescription description={instructions} />
|
||||
<ChallengeDescription instructions={instructions} />
|
||||
)}
|
||||
|
||||
<Spacer size='medium' />
|
||||
@@ -244,6 +259,43 @@ export const query = graphql`
|
||||
testString
|
||||
}
|
||||
}
|
||||
scene {
|
||||
setup {
|
||||
background
|
||||
characters {
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
}
|
||||
audio {
|
||||
filename
|
||||
startTime
|
||||
startTimestamp
|
||||
finishTimestamp
|
||||
}
|
||||
alwaysShowDialogue
|
||||
}
|
||||
commands {
|
||||
background
|
||||
character
|
||||
position {
|
||||
x
|
||||
y
|
||||
z
|
||||
}
|
||||
opacity
|
||||
startTime
|
||||
finishTime
|
||||
dialogue {
|
||||
text
|
||||
align
|
||||
}
|
||||
}
|
||||
}
|
||||
superBlock
|
||||
title
|
||||
translationPending
|
||||
|
||||
@@ -51,11 +51,6 @@ const msTrophy = path.resolve(
|
||||
'../../src/templates/Challenges/ms-trophy/show.tsx'
|
||||
);
|
||||
|
||||
const dialogue = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/dialogue/show.tsx'
|
||||
);
|
||||
|
||||
const fillInTheBlank = path.resolve(
|
||||
__dirname,
|
||||
'../../src/templates/Challenges/fill-in-the-blank/show.tsx'
|
||||
@@ -77,7 +72,6 @@ const views = {
|
||||
odin,
|
||||
exam,
|
||||
msTrophy,
|
||||
dialogue,
|
||||
fillInTheBlank,
|
||||
generic
|
||||
};
|
||||
|
||||
@@ -102,7 +102,7 @@ export const viewTypes = {
|
||||
[msTrophy]: 'msTrophy',
|
||||
[multipleChoice]: 'odin',
|
||||
[python]: 'modern',
|
||||
[dialogue]: 'dialogue',
|
||||
[dialogue]: 'generic', // TODO: use generic challengeType for dialogues
|
||||
[fillInTheBlank]: 'fillInTheBlank',
|
||||
[multifilePythonCertProject]: 'classic',
|
||||
[generic]: 'generic'
|
||||
|
||||
Reference in New Issue
Block a user