feat: add video lessons to db on submit + update UI (#38591)
* feat: add video lessons to db on submit + update UI * feat: delete CompletionVideoModal * feat: clean up component + add comments * feat: remove comment * feat: remove log * feat: remove log * fix: update buttons + fix some testing * fix: remove unused selector
This commit is contained in:
@ -1,301 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import noop from 'lodash/noop';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||
import { useStaticQuery, graphql } from 'gatsby';
|
||||
import SanitizedSpan from '../components/SanitizedSpan';
|
||||
|
||||
import Login from '../../../components/Header/components/Login';
|
||||
|
||||
import './completion-modal.css';
|
||||
|
||||
import {
|
||||
closeModal,
|
||||
submitChallenge,
|
||||
completedChallengesIds,
|
||||
isCompletionModalOpenSelector,
|
||||
challengeFilesSelector,
|
||||
challengeMetaSelector,
|
||||
lastBlockChalSubmitted
|
||||
} from '../redux';
|
||||
|
||||
import { isSignedInSelector, executeGA } from '../../../redux';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
challengeFilesSelector,
|
||||
challengeMetaSelector,
|
||||
completedChallengesIds,
|
||||
isCompletionModalOpenSelector,
|
||||
isSignedInSelector,
|
||||
(files, { title, id }, completedChallengesIds, isOpen, isSignedIn) => ({
|
||||
files,
|
||||
title,
|
||||
id,
|
||||
completedChallengesIds,
|
||||
isOpen,
|
||||
isSignedIn
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = function(dispatch) {
|
||||
const dispatchers = {
|
||||
close: () => dispatch(closeModal('completion')),
|
||||
submitChallenge: () => {
|
||||
dispatch(submitChallenge());
|
||||
},
|
||||
lastBlockChalSubmitted: () => {
|
||||
dispatch(lastBlockChalSubmitted());
|
||||
},
|
||||
executeGA
|
||||
};
|
||||
return () => dispatchers;
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
answers: PropTypes.array,
|
||||
blockName: PropTypes.string,
|
||||
close: PropTypes.func.isRequired,
|
||||
completedChallengesIds: PropTypes.array,
|
||||
currentBlockIds: PropTypes.array,
|
||||
executeGA: PropTypes.func,
|
||||
files: PropTypes.object.isRequired,
|
||||
id: PropTypes.string,
|
||||
isOpen: PropTypes.bool,
|
||||
isSignedIn: PropTypes.bool.isRequired,
|
||||
lastBlockChalSubmitted: PropTypes.func,
|
||||
question: PropTypes.string,
|
||||
solution: PropTypes.number,
|
||||
submitChallenge: PropTypes.func.isRequired,
|
||||
title: PropTypes.string
|
||||
};
|
||||
|
||||
export function getCompletedPercent(
|
||||
completedChallengesIds = [],
|
||||
currentBlockIds = [],
|
||||
currentChallengeId
|
||||
) {
|
||||
completedChallengesIds = completedChallengesIds.includes(currentChallengeId)
|
||||
? completedChallengesIds
|
||||
: [...completedChallengesIds, currentChallengeId];
|
||||
|
||||
const completedChallengesInBlock = completedChallengesIds.filter(id => {
|
||||
return currentBlockIds.includes(id);
|
||||
});
|
||||
|
||||
const completedPercent = Math.round(
|
||||
(completedChallengesInBlock.length / currentBlockIds.length) * 100
|
||||
);
|
||||
|
||||
return completedPercent > 100 ? 100 : completedPercent;
|
||||
}
|
||||
|
||||
export class CompletionModalInner extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleKeypress = this.handleKeypress.bind(this);
|
||||
}
|
||||
|
||||
state = {
|
||||
downloadURL: null,
|
||||
completedPercent: 0,
|
||||
selectedOption: 0,
|
||||
answer: 1,
|
||||
showWrong: false
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
const { files, isOpen } = props;
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const { downloadURL } = state;
|
||||
if (downloadURL) {
|
||||
URL.revokeObjectURL(downloadURL);
|
||||
}
|
||||
let newURL = null;
|
||||
if (Object.keys(files).length) {
|
||||
const filesForDownload = Object.keys(files)
|
||||
.map(key => files[key])
|
||||
.reduce((allFiles, { path, contents }) => {
|
||||
const beforeText = `** start of ${path} **\n\n`;
|
||||
const afterText = `\n\n** end of ${path} **\n\n`;
|
||||
allFiles +=
|
||||
files.length > 1 ? beforeText + contents + afterText : contents;
|
||||
return allFiles;
|
||||
}, '');
|
||||
const blob = new Blob([filesForDownload], {
|
||||
type: 'text/json'
|
||||
});
|
||||
newURL = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
const { completedChallengesIds, currentBlockIds, id, isSignedIn } = props;
|
||||
let completedPercent = isSignedIn
|
||||
? getCompletedPercent(completedChallengesIds, currentBlockIds, id)
|
||||
: 0;
|
||||
return { downloadURL: newURL, completedPercent: completedPercent };
|
||||
}
|
||||
|
||||
handleKeypress(e) {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
// Since Hotkeys also listens to Ctrl + Enter we have to stop this event
|
||||
// getting to it.
|
||||
e.stopPropagation();
|
||||
this.handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
if (this.props.solution - 1 === this.state.selectedOption) {
|
||||
this.setState({
|
||||
showWrong: false
|
||||
});
|
||||
this.props.submitChallenge();
|
||||
this.checkBlockCompletion();
|
||||
} else {
|
||||
this.setState({
|
||||
showWrong: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// check block completion for donation
|
||||
checkBlockCompletion() {
|
||||
if (
|
||||
this.state.completedPercent === 100 &&
|
||||
!this.props.completedChallengesIds.includes(this.props.id)
|
||||
) {
|
||||
this.props.lastBlockChalSubmitted();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.state.downloadURL) {
|
||||
URL.revokeObjectURL(this.state.downloadURL);
|
||||
}
|
||||
this.props.close();
|
||||
}
|
||||
|
||||
handleOptionChange = changeEvent => {
|
||||
console.log(this.state.selectedOption);
|
||||
this.setState({
|
||||
selectedOption: parseInt(changeEvent.target.value, 10)
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { close, isOpen, isSignedIn, question, answers } = this.props;
|
||||
|
||||
if (isOpen) {
|
||||
executeGA({ type: 'modal', data: '/completion-modal' });
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
animation={false}
|
||||
bsSize='lg'
|
||||
dialogClassName='challenge-success-modal'
|
||||
keyboard={true}
|
||||
onHide={close}
|
||||
onKeyDown={isOpen ? this.handleKeypress : noop}
|
||||
show={isOpen}
|
||||
>
|
||||
<Modal.Header
|
||||
className='challenge-list-header fcc-modal'
|
||||
closeButton={true}
|
||||
>
|
||||
<Modal.Title className='completion-message'>Video Quiz</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='question-modal-body'>
|
||||
<SanitizedSpan text={question} />
|
||||
<form>
|
||||
{answers.map((option, index) => (
|
||||
<div className='form-check'>
|
||||
<label>
|
||||
<input
|
||||
checked={this.state.selectedOption === index}
|
||||
name='quiz'
|
||||
onChange={this.handleOptionChange}
|
||||
type='radio'
|
||||
value={index}
|
||||
/>{' '}
|
||||
<SanitizedSpan text={option} />
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</form>
|
||||
<div
|
||||
style={{
|
||||
visibility: this.state.showWrong ? 'visible' : 'hidden'
|
||||
}}
|
||||
>
|
||||
Wrong. Try again.
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{isSignedIn ? null : (
|
||||
<Login
|
||||
block={true}
|
||||
bsSize='lg'
|
||||
bsStyle='primary'
|
||||
className='btn-cta'
|
||||
>
|
||||
Sign in to save your progress
|
||||
</Login>
|
||||
)}
|
||||
<Button
|
||||
block={true}
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
Submit answer <span className='hidden-xs'>(Ctrl + Enter)</span>
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CompletionModalInner.propTypes = propTypes;
|
||||
|
||||
const useCurrentBlockIds = blockName => {
|
||||
const {
|
||||
allChallengeNode: { edges }
|
||||
} = useStaticQuery(graphql`
|
||||
query getCurrentBlockNodesVid {
|
||||
allChallengeNode(sort: { fields: [superOrder, order, challengeOrder] }) {
|
||||
edges {
|
||||
node {
|
||||
fields {
|
||||
blockName
|
||||
}
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const currentBlockIds = edges
|
||||
.filter(edge => edge.node.fields.blockName === blockName)
|
||||
.map(edge => edge.node.id);
|
||||
return currentBlockIds;
|
||||
};
|
||||
|
||||
const CompletionModal = props => {
|
||||
const currentBlockIds = useCurrentBlockIds(props.blockName || '');
|
||||
return <CompletionModalInner currentBlockIds={currentBlockIds} {...props} />;
|
||||
};
|
||||
|
||||
CompletionModal.displayName = 'CompletionModal';
|
||||
CompletionModal.propTypes = propTypes;
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CompletionModal);
|
@ -50,8 +50,12 @@ function postChallenge(update, username) {
|
||||
}
|
||||
|
||||
function submitModern(type, state) {
|
||||
const challengeType = state.challenge.challengeMeta.challengeType;
|
||||
const tests = challengeTestsSelector(state);
|
||||
if (tests.length > 0 && tests.every(test => test.pass && !test.err)) {
|
||||
if (
|
||||
challengeType === 11 ||
|
||||
(tests.length > 0 && tests.every(test => test.pass && !test.err))
|
||||
) {
|
||||
if (type === types.checkChallenge) {
|
||||
return of({ type: 'this was a check challenge' });
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
// Package Utilities
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Grid, Col, Row } from '@freecodecamp/react-bootstrap';
|
||||
@ -6,24 +7,35 @@ import { bindActionCreators } from 'redux';
|
||||
import { graphql } from 'gatsby';
|
||||
import Helmet from 'react-helmet';
|
||||
import YouTube from 'react-youtube';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
// Local Utilities
|
||||
import SanitizedSpan from '../components/SanitizedSpan';
|
||||
import { ChallengeNode } from '../../../redux/propTypes';
|
||||
import LearnLayout from '../../../components/layouts/Learn';
|
||||
import ChallengeTitle from '../components/Challenge-Title';
|
||||
import ChallengeDescription from '../components/Challenge-Description';
|
||||
import Spacer from '../../../components/helpers/Spacer';
|
||||
import CompletionModal from '../components/CompletionModal';
|
||||
import Hotkeys from '../components/Hotkeys';
|
||||
import {
|
||||
isChallengeCompletedSelector,
|
||||
challengeMounted,
|
||||
updateChallengeMeta,
|
||||
openModal,
|
||||
updateProjectFormValues
|
||||
} from '../redux';
|
||||
|
||||
import LearnLayout from '../../../components/layouts/Learn';
|
||||
import ChallengeTitle from '../components/Challenge-Title';
|
||||
import ChallengeDescription from '../components/Challenge-Description';
|
||||
import Spacer from '../../../components/helpers/Spacer';
|
||||
import CompletionVideoModal from '../components/CompletionVideoModal';
|
||||
import HelpModal from '../components/HelpModal';
|
||||
import Hotkeys from '../components/Hotkeys';
|
||||
// Styles
|
||||
import './show.css';
|
||||
|
||||
const mapStateToProps = () => ({});
|
||||
// Redux Setup
|
||||
const mapStateToProps = createSelector(
|
||||
isChallengeCompletedSelector,
|
||||
isChallengeCompleted => ({
|
||||
isChallengeCompleted
|
||||
})
|
||||
);
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{
|
||||
@ -35,12 +47,14 @@ 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
|
||||
@ -49,12 +63,19 @@ const propTypes = {
|
||||
updateProjectFormValues: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
// Component
|
||||
export class Project extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
subtitles: ''
|
||||
subtitles: '',
|
||||
downloadURL: null,
|
||||
selectedOption: 0,
|
||||
answer: 1,
|
||||
showWrong: false
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -95,6 +116,25 @@ export class Project extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(solution, openCompletionModal) {
|
||||
if (solution - 1 === this.state.selectedOption) {
|
||||
this.setState({
|
||||
showWrong: false
|
||||
});
|
||||
openCompletionModal();
|
||||
} else {
|
||||
this.setState({
|
||||
showWrong: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleOptionChange = changeEvent => {
|
||||
this.setState({
|
||||
selectedOption: parseInt(changeEvent.target.value, 10)
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
data: {
|
||||
@ -109,10 +149,11 @@ export class Project extends Component {
|
||||
openCompletionModal,
|
||||
pageContext: {
|
||||
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
|
||||
}
|
||||
},
|
||||
isChallengeCompleted
|
||||
} = this.props;
|
||||
const blockNameTitle = `${blockName} - ${title}`;
|
||||
|
||||
const blockNameTitle = `${blockName} - ${title}`;
|
||||
return (
|
||||
<Hotkeys
|
||||
innerRef={c => (this._container = c)}
|
||||
@ -126,7 +167,9 @@ export class Project extends Component {
|
||||
<Row>
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<ChallengeTitle>{blockNameTitle}</ChallengeTitle>
|
||||
<ChallengeTitle isCompleted={isChallengeCompleted}>
|
||||
{blockNameTitle}
|
||||
</ChallengeTitle>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<YouTube
|
||||
onEnd={openCompletionModal}
|
||||
@ -150,24 +193,51 @@ export class Project extends Component {
|
||||
</div>
|
||||
<Spacer />
|
||||
<ChallengeDescription description={description} />
|
||||
|
||||
<Spacer />
|
||||
<SanitizedSpan text={text} />
|
||||
<Spacer />
|
||||
<div className='video-quiz-options'>
|
||||
{answers.map((option, index) => (
|
||||
<label className='video-quiz-option-label'>
|
||||
<input
|
||||
checked={this.state.selectedOption === index}
|
||||
className='video-quiz-input-hidden'
|
||||
name='quiz'
|
||||
onChange={this.handleOptionChange}
|
||||
type='radio'
|
||||
value={index}
|
||||
/>{' '}
|
||||
<span className='video-quiz-input-visible'>
|
||||
{this.state.selectedOption === index ? (
|
||||
<span className='video-quiz-selected-input'></span>
|
||||
) : null}
|
||||
</span>
|
||||
<SanitizedSpan text={option} />
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<Spacer />
|
||||
<Button
|
||||
block={true}
|
||||
bsSize='large'
|
||||
bsStyle='primary'
|
||||
onClick={openCompletionModal}
|
||||
onClick={() =>
|
||||
this.handleSubmit(solution, openCompletionModal)
|
||||
}
|
||||
>
|
||||
Complete
|
||||
Check your answer
|
||||
</Button>
|
||||
<Spacer />
|
||||
<div
|
||||
style={{
|
||||
visibility: this.state.showWrong ? 'visible' : 'hidden'
|
||||
}}
|
||||
>
|
||||
Wrong. Try again.
|
||||
</div>
|
||||
<Spacer size={2} />
|
||||
</Col>
|
||||
<CompletionVideoModal
|
||||
answers={answers}
|
||||
blockName={blockName}
|
||||
question={text}
|
||||
solution={solution}
|
||||
/>
|
||||
<HelpModal />
|
||||
<CompletionModal blockName={blockName} />
|
||||
</Row>
|
||||
</Grid>
|
||||
</LearnLayout>
|
||||
|
43
client/src/templates/Challenges/video/show.css
Normal file
43
client/src/templates/Challenges/video/show.css
Normal file
@ -0,0 +1,43 @@
|
||||
.video-quiz-options {
|
||||
padding: 1px 16px;
|
||||
background-color: var(--tertiary-background);
|
||||
}
|
||||
|
||||
.video-quiz-option-label {
|
||||
display: block;
|
||||
margin: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.video-quiz-input-hidden {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
.video-quiz-input-visible {
|
||||
margin-right: 15px;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--secondary-background);
|
||||
border: 2px solid var(--primary-color);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-quiz-selected-input {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
background-color: var(--primary-color);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
@ -125,5 +125,6 @@ exports.helpCategory = {
|
||||
'data-analysis-with-python': 'Certification Projects',
|
||||
'data-analysis-with-python-projects': 'Certification Projects',
|
||||
'machine-learning-with-python': 'Certification Projects',
|
||||
'machine-learning-with-python-projects': 'Certification Projects'
|
||||
'machine-learning-with-python-projects': 'Certification Projects',
|
||||
'python-for-everybody': 'Python'
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ function getSchemaForLang(lang) {
|
||||
challengeOrder: Joi.number(),
|
||||
challengeType: Joi.number()
|
||||
.min(0)
|
||||
.max(10)
|
||||
.max(11)
|
||||
.required(),
|
||||
checksum: Joi.number(),
|
||||
dashedName: Joi.string(),
|
||||
|
Reference in New Issue
Block a user