feat(client): completion modal progress bar (#37836)
Co-authored-by: Tom <20648924+moT01@users.noreply.github.com>
This commit is contained in:
5
client/package-lock.json
generated
5
client/package-lock.json
generated
@ -4074,6 +4074,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/better-queue-memory/-/better-queue-memory-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/better-queue-memory/-/better-queue-memory-1.0.4.tgz",
|
||||||
"integrity": "sha512-SWg5wFIShYffEmJpI6LgbL8/3Dqhku7xI1oEiy6FroP9DbcZlG0ZDjxvPdP9t7hTGW40IpIcC6zVoGT1oxjOuA=="
|
"integrity": "sha512-SWg5wFIShYffEmJpI6LgbL8/3Dqhku7xI1oEiy6FroP9DbcZlG0ZDjxvPdP9t7hTGW40IpIcC6zVoGT1oxjOuA=="
|
||||||
},
|
},
|
||||||
|
"bezier-easing": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz",
|
||||||
|
"integrity": "sha1-wE3+i5JtbsrKGBPWn/F5t8ICXYY="
|
||||||
|
},
|
||||||
"big.js": {
|
"big.js": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz",
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"@reach/router": "^1.2.1",
|
"@reach/router": "^1.2.1",
|
||||||
"algoliasearch": "^3.35.1",
|
"algoliasearch": "^3.35.1",
|
||||||
"axios": "^0.19.0",
|
"axios": "^0.19.0",
|
||||||
|
"bezier-easing": "^2.1.0",
|
||||||
"browser-cookies": "^1.2.0",
|
"browser-cookies": "^1.2.0",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"date-fns": "^1.30.1",
|
"date-fns": "^1.30.1",
|
||||||
|
@ -252,7 +252,11 @@ class ShowClassic extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { forumTopicId, title } = this.getChallenge();
|
const {
|
||||||
|
fields: { blockName },
|
||||||
|
forumTopicId,
|
||||||
|
title
|
||||||
|
} = this.getChallenge();
|
||||||
const {
|
const {
|
||||||
executeChallenge,
|
executeChallenge,
|
||||||
pageContext: {
|
pageContext: {
|
||||||
@ -298,7 +302,7 @@ class ShowClassic extends Component {
|
|||||||
testOutput={this.renderTestOutput()}
|
testOutput={this.renderTestOutput()}
|
||||||
/>
|
/>
|
||||||
</Media>
|
</Media>
|
||||||
<CompletionModal />
|
<CompletionModal blockName={blockName} />
|
||||||
<HelpModal />
|
<HelpModal />
|
||||||
<VideoModal videoUrl={this.getVideoUrl()} />
|
<VideoModal videoUrl={this.getVideoUrl()} />
|
||||||
<ResetModal />
|
<ResetModal />
|
||||||
|
@ -4,10 +4,11 @@ import noop from 'lodash/noop';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
import { Button, Modal } from '@freecodecamp/react-bootstrap';
|
||||||
|
import { useStaticQuery, graphql } from 'gatsby';
|
||||||
|
|
||||||
import ga from '../../../analytics';
|
import ga from '../../../analytics';
|
||||||
import Login from '../../../components/Header/components/Login';
|
import Login from '../../../components/Header/components/Login';
|
||||||
import GreenPass from '../../../assets/icons/GreenPass';
|
import CompletionModalBody from './CompletionModalBody';
|
||||||
|
|
||||||
import { dasherize } from '../../../../../utils/slugs';
|
import { dasherize } from '../../../../../utils/slugs';
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ import './completion-modal.css';
|
|||||||
import {
|
import {
|
||||||
closeModal,
|
closeModal,
|
||||||
submitChallenge,
|
submitChallenge,
|
||||||
|
completedChallengesIds,
|
||||||
isCompletionModalOpenSelector,
|
isCompletionModalOpenSelector,
|
||||||
successMessageSelector,
|
successMessageSelector,
|
||||||
challengeFilesSelector,
|
challengeFilesSelector,
|
||||||
@ -27,12 +29,22 @@ import { isSignedInSelector } from '../../../redux';
|
|||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
challengeFilesSelector,
|
challengeFilesSelector,
|
||||||
challengeMetaSelector,
|
challengeMetaSelector,
|
||||||
|
completedChallengesIds,
|
||||||
isCompletionModalOpenSelector,
|
isCompletionModalOpenSelector,
|
||||||
isSignedInSelector,
|
isSignedInSelector,
|
||||||
successMessageSelector,
|
successMessageSelector,
|
||||||
(files, { title }, isOpen, isSignedIn, message) => ({
|
(
|
||||||
|
files,
|
||||||
|
{ title, id },
|
||||||
|
completedChallengesIds,
|
||||||
|
isOpen,
|
||||||
|
isSignedIn,
|
||||||
|
message
|
||||||
|
) => ({
|
||||||
files,
|
files,
|
||||||
title,
|
title,
|
||||||
|
id,
|
||||||
|
completedChallengesIds,
|
||||||
isOpen,
|
isOpen,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
message
|
message
|
||||||
@ -59,9 +71,13 @@ const mapDispatchToProps = function(dispatch) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
blockName: PropTypes.string,
|
||||||
close: PropTypes.func.isRequired,
|
close: PropTypes.func.isRequired,
|
||||||
|
completedChallengesIds: PropTypes.array,
|
||||||
|
currentBlockIds: PropTypes.array,
|
||||||
files: PropTypes.object.isRequired,
|
files: PropTypes.object.isRequired,
|
||||||
handleKeypress: PropTypes.func.isRequired,
|
handleKeypress: PropTypes.func.isRequired,
|
||||||
|
id: PropTypes.string,
|
||||||
isOpen: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
isSignedIn: PropTypes.bool.isRequired,
|
isSignedIn: PropTypes.bool.isRequired,
|
||||||
message: PropTypes.string,
|
message: PropTypes.string,
|
||||||
@ -69,7 +85,27 @@ const propTypes = {
|
|||||||
title: PropTypes.string
|
title: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export class CompletionModal extends Component {
|
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 {
|
||||||
state = {
|
state = {
|
||||||
downloadURL: null
|
downloadURL: null
|
||||||
};
|
};
|
||||||
@ -111,7 +147,11 @@ export class CompletionModal extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
blockName = '',
|
||||||
close,
|
close,
|
||||||
|
completedChallengesIds = [],
|
||||||
|
currentBlockIds = [],
|
||||||
|
id = '',
|
||||||
isOpen,
|
isOpen,
|
||||||
isSignedIn,
|
isSignedIn,
|
||||||
submitChallenge,
|
submitChallenge,
|
||||||
@ -119,6 +159,11 @@ export class CompletionModal extends Component {
|
|||||||
message,
|
message,
|
||||||
title
|
title
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const completedPercent = !isSignedIn
|
||||||
|
? 0
|
||||||
|
: getCompletedPercent(completedChallengesIds, currentBlockIds, id);
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
ga.modalview('/completion-modal');
|
ga.modalview('/completion-modal');
|
||||||
}
|
}
|
||||||
@ -137,12 +182,13 @@ export class CompletionModal extends Component {
|
|||||||
className='challenge-list-header fcc-modal'
|
className='challenge-list-header fcc-modal'
|
||||||
closeButton={true}
|
closeButton={true}
|
||||||
>
|
>
|
||||||
<Modal.Title className='text-center'>{message}</Modal.Title>
|
<Modal.Title className='completion-message'>{message}</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body className='completion-modal-body'>
|
<Modal.Body className='completion-modal-body'>
|
||||||
<div className='success-icon-wrapper'>
|
<CompletionModalBody
|
||||||
<GreenPass />
|
blockName={blockName}
|
||||||
</div>
|
completedPercent={completedPercent}
|
||||||
|
/>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button
|
<Button
|
||||||
@ -182,6 +228,37 @@ export class CompletionModal extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CompletionModalInner.propTypes = propTypes;
|
||||||
|
|
||||||
|
const useCurrentBlockIds = blockName => {
|
||||||
|
const {
|
||||||
|
allChallengeNode: { edges }
|
||||||
|
} = useStaticQuery(graphql`
|
||||||
|
query getCurrentBlockNodes {
|
||||||
|
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.displayName = 'CompletionModal';
|
||||||
CompletionModal.propTypes = propTypes;
|
CompletionModal.propTypes = propTypes;
|
||||||
|
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
/* global expect */
|
||||||
|
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
|
||||||
|
import { getCompletedPercent } from './CompletionModal';
|
||||||
|
|
||||||
|
const completedChallengesIds = ['1', '3', '5'],
|
||||||
|
currentBlockIds = ['1', '3', '5', '7'],
|
||||||
|
id = '7',
|
||||||
|
fakeId = '12345',
|
||||||
|
fakeCompletedChallengesIds = ['1', '3', '5', '7', '8'];
|
||||||
|
|
||||||
|
describe('<CompletionModal />', () => {
|
||||||
|
describe('getCompletedPercent', () => {
|
||||||
|
it('returns 0 if no challenges have been completed', () => {
|
||||||
|
expect(getCompletedPercent([], currentBlockIds, fakeId)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 25 if one out of four challenges are complete', () => {
|
||||||
|
expect(getCompletedPercent([], currentBlockIds, currentBlockIds[1])).toBe(
|
||||||
|
25
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 75 if three out of four challenges are complete', () => {
|
||||||
|
expect(
|
||||||
|
getCompletedPercent(
|
||||||
|
completedChallengesIds,
|
||||||
|
currentBlockIds,
|
||||||
|
completedChallengesIds[0]
|
||||||
|
)
|
||||||
|
).toBe(75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 100 if all challenges have been completed', () => {
|
||||||
|
expect(
|
||||||
|
getCompletedPercent(completedChallengesIds, currentBlockIds, id)
|
||||||
|
).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 100 if more challenges have been complete than exist', () => {
|
||||||
|
expect(
|
||||||
|
getCompletedPercent(fakeCompletedChallengesIds, currentBlockIds, id)
|
||||||
|
).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,97 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import BezierEasing from 'bezier-easing';
|
||||||
|
import GreenPass from '../../../assets/icons/GreenPass';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
blockName: PropTypes.string,
|
||||||
|
completedPercent: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
export class CompletionModalBody extends PureComponent {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
progressInterval: null,
|
||||||
|
shownPercent: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
this.animateProgressBar = this.animateProgressBar.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
animateProgressBar(completedPercent) {
|
||||||
|
const easing = BezierEasing(0.2, 0.5, 0.4, 1);
|
||||||
|
|
||||||
|
if (completedPercent > 100) completedPercent = 100;
|
||||||
|
if (completedPercent < 0) completedPercent = 0;
|
||||||
|
|
||||||
|
const transitionLength = completedPercent * 10 + 750;
|
||||||
|
const intervalLength = 10;
|
||||||
|
const intervalsToFinish = transitionLength / intervalLength;
|
||||||
|
const amountPerInterval = completedPercent / intervalsToFinish;
|
||||||
|
let percent = 0;
|
||||||
|
|
||||||
|
const myInterval = setInterval(() => {
|
||||||
|
percent += amountPerInterval;
|
||||||
|
|
||||||
|
if (percent > completedPercent) percent = completedPercent;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
shownPercent: Math.round(
|
||||||
|
completedPercent * easing(percent / completedPercent)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (percent >= completedPercent) clearInterval(myInterval);
|
||||||
|
}, intervalLength);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
progressInterval: myInterval
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearInterval(this.state.progressInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { blockName, completedPercent } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='completion-challenge-details'>
|
||||||
|
<GreenPass
|
||||||
|
className='completion-success-icon'
|
||||||
|
onAnimationEnd={() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.animateProgressBar(completedPercent);
|
||||||
|
}, 50);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='completion-block-details'>
|
||||||
|
<div className='completion-block-name'>{blockName}</div>
|
||||||
|
<div className='progress-bar-wrap'>
|
||||||
|
<div className='progress-bar-background'>
|
||||||
|
{this.state.shownPercent}% complete
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='progress-bar-percent'
|
||||||
|
style={{ width: this.state.shownPercent + '%' }}
|
||||||
|
>
|
||||||
|
<div className='progress-bar-foreground'>
|
||||||
|
{this.state.shownPercent}% complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompletionModalBody.displayName = 'CompletionModalBody';
|
||||||
|
CompletionModalBody.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default CompletionModalBody;
|
@ -0,0 +1,68 @@
|
|||||||
|
/* global jest, expect */
|
||||||
|
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
import React from 'react';
|
||||||
|
import { render, fireEvent } from '@testing-library/react';
|
||||||
|
|
||||||
|
import CompletionModalBody from './CompletionModalBody';
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
blockName: 'Basic HTML and HTML5',
|
||||||
|
completedPercent: Math.floor(Math.random() * 101)
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<CompletionModalBody />', () => {
|
||||||
|
test('matches snapshot', () => {
|
||||||
|
const { container } = render(<CompletionModalBody {...props} />);
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('progress-bar', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders with 0% complete shown initially', () => {
|
||||||
|
const { getAllByText } = render(<CompletionModalBody {...props} />);
|
||||||
|
expect(getAllByText('0% complete').length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders with 0% width initially', () => {
|
||||||
|
const { container } = render(<CompletionModalBody {...props} />);
|
||||||
|
expect(container.querySelector('.progress-bar-percent')).toHaveAttribute(
|
||||||
|
'style',
|
||||||
|
'width: 0%;'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shows the correct percent after animation', () => {
|
||||||
|
const { container, getAllByText } = render(
|
||||||
|
<CompletionModalBody {...props} />
|
||||||
|
);
|
||||||
|
const progressBars = getAllByText('0% complete');
|
||||||
|
fireEvent.animationEnd(
|
||||||
|
container.querySelector('.completion-success-icon')
|
||||||
|
);
|
||||||
|
jest.runAllTimers();
|
||||||
|
progressBars.forEach(bar =>
|
||||||
|
expect(bar).toHaveTextContent(`${props.completedPercent}% complete`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('has the correct width after animation', () => {
|
||||||
|
const { container } = render(<CompletionModalBody {...props} />);
|
||||||
|
|
||||||
|
fireEvent.animationEnd(
|
||||||
|
container.querySelector('.completion-success-icon')
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
|
expect(container.querySelector('.progress-bar-percent')).toHaveAttribute(
|
||||||
|
'style',
|
||||||
|
`width: ${props.completedPercent}%;`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,92 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<CompletionModalBody /> matches snapshot 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="completion-challenge-details"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="sr-only"
|
||||||
|
>
|
||||||
|
Passed
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="completion-success-icon"
|
||||||
|
height="50"
|
||||||
|
viewBox="0 0 200 200"
|
||||||
|
width="50"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<title>
|
||||||
|
Passed
|
||||||
|
</title>
|
||||||
|
<circle
|
||||||
|
cx="100"
|
||||||
|
cy="99"
|
||||||
|
fill="var(--primary-color)"
|
||||||
|
r="95"
|
||||||
|
stroke="var(--primary-color)"
|
||||||
|
stroke-dasharray="null"
|
||||||
|
stroke-linecap="null"
|
||||||
|
stroke-linejoin="null"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
fill="var(--primary-background)"
|
||||||
|
height="30"
|
||||||
|
stroke="var(--primary-background)"
|
||||||
|
stroke-dasharray="null"
|
||||||
|
stroke-linecap="null"
|
||||||
|
stroke-linejoin="null"
|
||||||
|
transform="rotate(-45, 120, 106.321)"
|
||||||
|
width="128.85878"
|
||||||
|
x="55.57059"
|
||||||
|
y="91.32089"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
fill="var(--primary-background)"
|
||||||
|
height="30"
|
||||||
|
stroke="var(--primary-background)"
|
||||||
|
stroke-dasharray="null"
|
||||||
|
stroke-linecap="null"
|
||||||
|
stroke-linejoin="null"
|
||||||
|
transform="rotate(45, 66.75, 123.75)"
|
||||||
|
width="80.66548"
|
||||||
|
x="26.41726"
|
||||||
|
y="108.75"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="completion-block-details"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="completion-block-name"
|
||||||
|
>
|
||||||
|
Basic HTML and HTML5
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="progress-bar-wrap"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="progress-bar-background"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
% complete
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="progress-bar-percent"
|
||||||
|
style="width: 0%;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="progress-bar-foreground"
|
||||||
|
>
|
||||||
|
0
|
||||||
|
% complete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -1,17 +1,119 @@
|
|||||||
|
.completion-message {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.completion-modal-body {
|
.completion-modal-body {
|
||||||
height: 45vh;
|
min-height: 400px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-challenge-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-icon-wrapper > svg {
|
.completion-success-icon {
|
||||||
height: 30vh;
|
width: 200px;
|
||||||
width: 30vh;
|
height: 200px;
|
||||||
|
transform: scale(1.5);
|
||||||
|
opacity: 0;
|
||||||
|
animation: success-icon-animation 150ms linear 100ms forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
@keyframes success-icon-animation {
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-block-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-block-name {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrap {
|
||||||
|
width: 400px;
|
||||||
|
height: 50px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-background {
|
||||||
|
width: 400px;
|
||||||
|
height: 50px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: 3px solid var(--primary-color);
|
||||||
|
background-color: var(--quaternary-background);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-percent {
|
||||||
|
width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
transition: width 0ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-foreground {
|
||||||
|
color: var(--primary-background);
|
||||||
|
width: 400px;
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 991px) {
|
||||||
.challenge-success-modal .btn-lg {
|
.challenge-success-modal .btn-lg {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.completion-modal-body {
|
||||||
|
min-height: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-wrap,
|
||||||
|
.progress-bar-background,
|
||||||
|
.progress-bar-foreground {
|
||||||
|
width: 260px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-success-icon {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-message {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-challenge-name,
|
||||||
|
.completion-block-name {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,7 @@ export class BackEnd extends Component {
|
|||||||
<TestSuite tests={tests} />
|
<TestSuite tests={tests} />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
</Col>
|
</Col>
|
||||||
<CompletionModal />
|
<CompletionModal blockName={blockName} />
|
||||||
<HelpModal />
|
<HelpModal />
|
||||||
</Row>
|
</Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -136,7 +136,7 @@ export class Project extends Component {
|
|||||||
<br />
|
<br />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
</Col>
|
</Col>
|
||||||
<CompletionModal />
|
<CompletionModal blockName={blockName} />
|
||||||
<HelpModal />
|
<HelpModal />
|
||||||
</Row>
|
</Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -162,6 +162,8 @@ export const challengeFilesSelector = state => state[ns].challengeFiles;
|
|||||||
export const challengeMetaSelector = state => state[ns].challengeMeta;
|
export const challengeMetaSelector = state => state[ns].challengeMeta;
|
||||||
export const challengeTestsSelector = state => state[ns].challengeTests;
|
export const challengeTestsSelector = state => state[ns].challengeTests;
|
||||||
export const consoleOutputSelector = state => state[ns].consoleOut;
|
export const consoleOutputSelector = state => state[ns].consoleOut;
|
||||||
|
export const completedChallengesIds = state =>
|
||||||
|
completedChallengesSelector(state).map(node => node.id);
|
||||||
export const isChallengeCompletedSelector = state => {
|
export const isChallengeCompletedSelector = state => {
|
||||||
const completedChallenges = completedChallengesSelector(state);
|
const completedChallenges = completedChallengesSelector(state);
|
||||||
const { id: currentChallengeId } = challengeMetaSelector(state);
|
const { id: currentChallengeId } = challengeMetaSelector(state);
|
||||||
|
Reference in New Issue
Block a user