2018-02-19 20:32:14 +00:00
|
|
|
import React, { PureComponent } from 'react';
|
|
|
|
|
import PropTypes from 'prop-types';
|
|
|
|
|
import { createSelector } from 'reselect';
|
|
|
|
|
import { connect } from 'react-redux';
|
|
|
|
|
import format from 'date-fns/format';
|
2018-05-21 16:21:15 +01:00
|
|
|
import { find, reverse, sortBy } from 'lodash';
|
2018-02-19 20:32:14 +00:00
|
|
|
import {
|
2018-05-21 16:21:15 +01:00
|
|
|
Button,
|
|
|
|
|
Modal,
|
2018-02-19 20:32:14 +00:00
|
|
|
Table
|
|
|
|
|
} from 'react-bootstrap';
|
|
|
|
|
|
|
|
|
|
import { challengeIdToNameMapSelector } from '../../../entities';
|
2018-05-15 06:12:05 +01:00
|
|
|
import { userByNameSelector } from '../../../redux';
|
2018-02-19 20:32:14 +00:00
|
|
|
import { homeURL } from '../../../../utils/constantStrings.json';
|
|
|
|
|
import blockNameify from '../../../utils/blockNameify';
|
|
|
|
|
import { Link } from '../../../Router';
|
2018-05-21 16:21:15 +01:00
|
|
|
import { FullWidthRow } from '../../../helperComponents';
|
|
|
|
|
import SolutionViewer from '../../Settings/components/SolutionViewer.jsx';
|
2018-02-19 20:32:14 +00:00
|
|
|
|
|
|
|
|
const mapStateToProps = createSelector(
|
|
|
|
|
challengeIdToNameMapSelector,
|
|
|
|
|
userByNameSelector,
|
|
|
|
|
(
|
|
|
|
|
idToNameMap,
|
2018-05-21 16:21:15 +01:00
|
|
|
{ completedChallenges: completedMap = [], username }
|
2018-02-19 20:32:14 +00:00
|
|
|
) => ({
|
|
|
|
|
completedMap,
|
2018-05-21 16:21:15 +01:00
|
|
|
idToNameMap,
|
|
|
|
|
username
|
2018-02-19 20:32:14 +00:00
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const propTypes = {
|
2018-05-15 14:56:26 +01:00
|
|
|
completedMap: PropTypes.arrayOf(
|
|
|
|
|
PropTypes.shape({
|
|
|
|
|
id: PropTypes.string,
|
|
|
|
|
completedDate: PropTypes.number,
|
2018-05-21 16:21:15 +01:00
|
|
|
challengeType: PropTypes.number,
|
|
|
|
|
solution: PropTypes.string,
|
|
|
|
|
files: PropTypes.shape({
|
|
|
|
|
ext: PropTypes.string,
|
|
|
|
|
contents: PropTypes.string
|
|
|
|
|
})
|
2018-05-15 14:56:26 +01:00
|
|
|
})
|
|
|
|
|
),
|
2018-05-21 16:21:15 +01:00
|
|
|
idToNameMap: PropTypes.objectOf(PropTypes.string),
|
|
|
|
|
username: PropTypes.string
|
2018-02-19 20:32:14 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class Timeline extends PureComponent {
|
|
|
|
|
constructor(props) {
|
|
|
|
|
super(props);
|
|
|
|
|
|
2018-05-21 16:21:15 +01:00
|
|
|
this.state = {
|
|
|
|
|
solutionToView: null,
|
|
|
|
|
solutionOpen: false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.closeSolution = this.closeSolution.bind(this);
|
2018-02-19 20:32:14 +00:00
|
|
|
this.renderCompletion = this.renderCompletion.bind(this);
|
2018-05-21 16:21:15 +01:00
|
|
|
this.viewSolution = this.viewSolution.bind(this);
|
2018-02-19 20:32:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
renderCompletion(completed) {
|
|
|
|
|
const { idToNameMap } = this.props;
|
2018-05-21 16:21:15 +01:00
|
|
|
const { id, completedDate, solution, files } = completed;
|
|
|
|
|
const challengeDashedName = idToNameMap[id];
|
2018-02-19 20:32:14 +00:00
|
|
|
return (
|
|
|
|
|
<tr key={ id }>
|
|
|
|
|
<td>
|
2018-05-21 16:21:15 +01:00
|
|
|
<a href={`/challenges/${challengeDashedName}`}>
|
|
|
|
|
{ blockNameify(challengeDashedName) }
|
|
|
|
|
</a>
|
|
|
|
|
</td>
|
|
|
|
|
<td className='text-center'>
|
2018-02-19 20:32:14 +00:00
|
|
|
<time dateTime={ format(completedDate, 'YYYY-MM-DDTHH:MM:SSZ') }>
|
|
|
|
|
{
|
2018-05-21 16:21:15 +01:00
|
|
|
format(completedDate, 'MMMM D, YYYY')
|
2018-02-19 20:32:14 +00:00
|
|
|
}
|
|
|
|
|
</time>
|
|
|
|
|
</td>
|
2018-05-21 16:21:15 +01:00
|
|
|
<td>
|
|
|
|
|
{/* eslint-disable no-nested-ternary */
|
|
|
|
|
files ? (
|
|
|
|
|
<Button
|
|
|
|
|
block={ true }
|
|
|
|
|
bsStyle='primary'
|
|
|
|
|
onClick={ () => this.viewSolution(id) }
|
|
|
|
|
>
|
|
|
|
|
View Solution
|
|
|
|
|
</Button>
|
|
|
|
|
) : solution ? (
|
|
|
|
|
<Button
|
|
|
|
|
block={ true }
|
|
|
|
|
bsStyle='primary'
|
|
|
|
|
href={solution}
|
|
|
|
|
target='_blank'
|
|
|
|
|
>
|
|
|
|
|
View Solution
|
|
|
|
|
</Button>
|
|
|
|
|
) : ''
|
|
|
|
|
}
|
|
|
|
|
</td>
|
2018-02-19 20:32:14 +00:00
|
|
|
</tr>
|
|
|
|
|
);
|
|
|
|
|
}
|
2018-05-21 16:21:15 +01:00
|
|
|
viewSolution(id) {
|
|
|
|
|
this.setState(state => ({
|
|
|
|
|
...state,
|
|
|
|
|
solutionToView: id,
|
|
|
|
|
solutionOpen: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
closeSolution() {
|
|
|
|
|
this.setState(state => ({
|
|
|
|
|
...state,
|
|
|
|
|
solutionToView: null,
|
|
|
|
|
solutionOpen: false
|
|
|
|
|
}));
|
|
|
|
|
}
|
2018-02-19 20:32:14 +00:00
|
|
|
|
|
|
|
|
render() {
|
2018-05-21 16:21:15 +01:00
|
|
|
const { completedMap, idToNameMap, username } = this.props;
|
|
|
|
|
const { solutionToView: id, solutionOpen } = this.state;
|
2018-02-19 20:32:14 +00:00
|
|
|
if (!Object.keys(idToNameMap).length) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<FullWidthRow>
|
|
|
|
|
<h2 className='text-center'>Timeline</h2>
|
|
|
|
|
{
|
2018-05-15 14:56:26 +01:00
|
|
|
completedMap.length === 0 ?
|
2018-02-19 20:32:14 +00:00
|
|
|
<p className='text-center'>
|
|
|
|
|
No challenges have been completed yet.
|
|
|
|
|
<Link to={ homeURL }>
|
|
|
|
|
Get started here.
|
|
|
|
|
</Link>
|
|
|
|
|
</p> :
|
|
|
|
|
<Table condensed={true} striped={true}>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>Challenge</th>
|
2018-05-21 16:21:15 +01:00
|
|
|
<th className='text-center'>First Completed</th>
|
2018-02-19 20:32:14 +00:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{
|
|
|
|
|
reverse(
|
|
|
|
|
sortBy(
|
2018-05-15 14:56:26 +01:00
|
|
|
completedMap,
|
2018-02-19 20:32:14 +00:00
|
|
|
[ 'completedDate' ]
|
2018-05-15 14:56:26 +01:00
|
|
|
).filter(({id}) => id in idToNameMap)
|
2018-02-19 20:32:14 +00:00
|
|
|
)
|
|
|
|
|
.map(this.renderCompletion)
|
|
|
|
|
}
|
|
|
|
|
</tbody>
|
|
|
|
|
</Table>
|
|
|
|
|
}
|
2018-05-21 16:21:15 +01:00
|
|
|
{
|
|
|
|
|
id &&
|
|
|
|
|
<Modal
|
|
|
|
|
aria-labelledby='contained-modal-title'
|
|
|
|
|
onHide={this.closeSolution}
|
|
|
|
|
show={ solutionOpen }
|
|
|
|
|
>
|
|
|
|
|
<Modal.Header closeButton={ true }>
|
|
|
|
|
<Modal.Title id='contained-modal-title'>
|
|
|
|
|
{ `${username}'s Solution to ${blockNameify(idToNameMap[id])}` }
|
|
|
|
|
</Modal.Title>
|
|
|
|
|
</Modal.Header>
|
|
|
|
|
<Modal.Body>
|
|
|
|
|
<SolutionViewer
|
|
|
|
|
solution={
|
|
|
|
|
find(completedMap, ({id: completedId}) => completedId === id)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</Modal.Body>
|
|
|
|
|
<Modal.Footer>
|
|
|
|
|
<Button onClick={this.closeSolution}>Close</Button>
|
|
|
|
|
</Modal.Footer>
|
|
|
|
|
</Modal>
|
|
|
|
|
}
|
2018-02-19 20:32:14 +00:00
|
|
|
</FullWidthRow>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Timeline.displayName = 'Timeline';
|
|
|
|
|
Timeline.propTypes = propTypes;
|
|
|
|
|
|
2018-05-15 06:12:05 +01:00
|
|
|
export default connect(mapStateToProps)(Timeline);
|