refactor(client): use generic component for dialogues (#56752)

This commit is contained in:
Oliver Eyton-Williams
2024-10-24 16:23:23 +02:00
committed by GitHub
parent 5a7855039b
commit ff7e0cf682
4 changed files with 58 additions and 382 deletions

View File

@@ -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
}
}
}
}
}
}
`;

View File

@@ -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

View File

@@ -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
};

View File

@@ -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'