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) {
|
function submitModern(type, state) {
|
||||||
|
const challengeType = state.challenge.challengeMeta.challengeType;
|
||||||
const tests = challengeTestsSelector(state);
|
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) {
|
if (type === types.checkChallenge) {
|
||||||
return of({ type: 'this was a check challenge' });
|
return of({ type: 'this was a check challenge' });
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// Package Utilities
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Button, Grid, Col, Row } from '@freecodecamp/react-bootstrap';
|
import { Button, Grid, Col, Row } from '@freecodecamp/react-bootstrap';
|
||||||
@ -6,24 +7,35 @@ import { bindActionCreators } from 'redux';
|
|||||||
import { graphql } from 'gatsby';
|
import { graphql } from 'gatsby';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
import YouTube from 'react-youtube';
|
import YouTube from 'react-youtube';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
// Local Utilities
|
||||||
|
import SanitizedSpan from '../components/SanitizedSpan';
|
||||||
import { ChallengeNode } from '../../../redux/propTypes';
|
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 {
|
import {
|
||||||
|
isChallengeCompletedSelector,
|
||||||
challengeMounted,
|
challengeMounted,
|
||||||
updateChallengeMeta,
|
updateChallengeMeta,
|
||||||
openModal,
|
openModal,
|
||||||
updateProjectFormValues
|
updateProjectFormValues
|
||||||
} from '../redux';
|
} from '../redux';
|
||||||
|
|
||||||
import LearnLayout from '../../../components/layouts/Learn';
|
// Styles
|
||||||
import ChallengeTitle from '../components/Challenge-Title';
|
import './show.css';
|
||||||
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';
|
|
||||||
|
|
||||||
const mapStateToProps = () => ({});
|
// Redux Setup
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
isChallengeCompletedSelector,
|
||||||
|
isChallengeCompleted => ({
|
||||||
|
isChallengeCompleted
|
||||||
|
})
|
||||||
|
);
|
||||||
const mapDispatchToProps = dispatch =>
|
const mapDispatchToProps = dispatch =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{
|
{
|
||||||
@ -35,12 +47,14 @@ const mapDispatchToProps = dispatch =>
|
|||||||
dispatch
|
dispatch
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Proptypes
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
challengeMounted: PropTypes.func.isRequired,
|
challengeMounted: PropTypes.func.isRequired,
|
||||||
data: PropTypes.shape({
|
data: PropTypes.shape({
|
||||||
challengeNode: ChallengeNode
|
challengeNode: ChallengeNode
|
||||||
}),
|
}),
|
||||||
description: PropTypes.string,
|
description: PropTypes.string,
|
||||||
|
isChallengeCompleted: PropTypes.bool,
|
||||||
openCompletionModal: PropTypes.func.isRequired,
|
openCompletionModal: PropTypes.func.isRequired,
|
||||||
pageContext: PropTypes.shape({
|
pageContext: PropTypes.shape({
|
||||||
challengeMeta: PropTypes.object
|
challengeMeta: PropTypes.object
|
||||||
@ -49,12 +63,19 @@ const propTypes = {
|
|||||||
updateProjectFormValues: PropTypes.func.isRequired
|
updateProjectFormValues: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Component
|
||||||
export class Project extends Component {
|
export class Project extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
subtitles: ''
|
subtitles: '',
|
||||||
|
downloadURL: null,
|
||||||
|
selectedOption: 0,
|
||||||
|
answer: 1,
|
||||||
|
showWrong: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
@ -109,10 +149,11 @@ export class Project extends Component {
|
|||||||
openCompletionModal,
|
openCompletionModal,
|
||||||
pageContext: {
|
pageContext: {
|
||||||
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
|
challengeMeta: { introPath, nextChallengePath, prevChallengePath }
|
||||||
}
|
},
|
||||||
|
isChallengeCompleted
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const blockNameTitle = `${blockName} - ${title}`;
|
|
||||||
|
|
||||||
|
const blockNameTitle = `${blockName} - ${title}`;
|
||||||
return (
|
return (
|
||||||
<Hotkeys
|
<Hotkeys
|
||||||
innerRef={c => (this._container = c)}
|
innerRef={c => (this._container = c)}
|
||||||
@ -126,7 +167,9 @@ export class Project extends Component {
|
|||||||
<Row>
|
<Row>
|
||||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ChallengeTitle>{blockNameTitle}</ChallengeTitle>
|
<ChallengeTitle isCompleted={isChallengeCompleted}>
|
||||||
|
{blockNameTitle}
|
||||||
|
</ChallengeTitle>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: 'center' }}>
|
||||||
<YouTube
|
<YouTube
|
||||||
onEnd={openCompletionModal}
|
onEnd={openCompletionModal}
|
||||||
@ -150,24 +193,51 @@ export class Project extends Component {
|
|||||||
</div>
|
</div>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ChallengeDescription description={description} />
|
<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
|
<Button
|
||||||
block={true}
|
block={true}
|
||||||
bsSize='large'
|
bsSize='large'
|
||||||
bsStyle='primary'
|
bsStyle='primary'
|
||||||
onClick={openCompletionModal}
|
onClick={() =>
|
||||||
|
this.handleSubmit(solution, openCompletionModal)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Complete
|
Check your answer
|
||||||
</Button>
|
</Button>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
visibility: this.state.showWrong ? 'visible' : 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Wrong. Try again.
|
||||||
|
</div>
|
||||||
|
<Spacer size={2} />
|
||||||
</Col>
|
</Col>
|
||||||
<CompletionVideoModal
|
<CompletionModal blockName={blockName} />
|
||||||
answers={answers}
|
|
||||||
blockName={blockName}
|
|
||||||
question={text}
|
|
||||||
solution={solution}
|
|
||||||
/>
|
|
||||||
<HelpModal />
|
|
||||||
</Row>
|
</Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
</LearnLayout>
|
</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': 'Certification Projects',
|
||||||
'data-analysis-with-python-projects': 'Certification Projects',
|
'data-analysis-with-python-projects': 'Certification Projects',
|
||||||
'machine-learning-with-python': '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(),
|
challengeOrder: Joi.number(),
|
||||||
challengeType: Joi.number()
|
challengeType: Joi.number()
|
||||||
.min(0)
|
.min(0)
|
||||||
.max(10)
|
.max(11)
|
||||||
.required(),
|
.required(),
|
||||||
checksum: Joi.number(),
|
checksum: Joi.number(),
|
||||||
dashedName: Joi.string(),
|
dashedName: Joi.string(),
|
||||||
|
Reference in New Issue
Block a user