diff --git a/client/src/components/profile/components/TimeLine.js b/client/src/components/profile/components/TimeLine.js index 81f1abcf4b..be93b87895 100644 --- a/client/src/components/profile/components/TimeLine.js +++ b/client/src/components/profile/components/TimeLine.js @@ -1,13 +1,16 @@ -import React, { Component } from 'react'; +import React, { Component, useMemo } from 'react'; import PropTypes from 'prop-types'; import format from 'date-fns/format'; import { find, reverse, sortBy } from 'lodash'; import { Button, Modal, Table } from '@freecodecamp/react-bootstrap'; import { Link, useStaticQuery, graphql } from 'gatsby'; +import TimelinePagination from './TimelinePagination'; import { FullWidthRow } from '../../helpers'; import SolutionViewer from '../../settings/SolutionViewer'; import { challengeTypes } from '../../../../utils/challengeTypes'; +// Items per page in timeline. +const ITEMS_PER_PAGE = 15; const propTypes = { completedMap: PropTypes.arrayOf( @@ -24,13 +27,30 @@ const propTypes = { ) }) ), + username: PropTypes.string +}; + +const innerPropTypes = { + ...propTypes, idToNameMap: PropTypes.objectOf( PropTypes.shape({ challengePath: PropTypes.string, challengeTitle: PropTypes.string }) - ), - username: PropTypes.string + ).isRequired, + sortedTimeline: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + completedDate: PropTypes.number, + files: PropTypes.arrayOf( + PropTypes.shape({ + ext: PropTypes.string, + contents: PropTypes.string + }) + ) + }) + ).isRequired, + totalPages: PropTypes.number.isRequired }; class TimelineInner extends Component { @@ -39,12 +59,17 @@ class TimelineInner extends Component { this.state = { solutionToView: null, - solutionOpen: false + solutionOpen: false, + pageNo: 1 }; this.closeSolution = this.closeSolution.bind(this); this.renderCompletion = this.renderCompletion.bind(this); this.viewSolution = this.viewSolution.bind(this); + this.firstPage = this.firstPage.bind(this); + this.prevPage = this.prevPage.bind(this); + this.nextPage = this.nextPage.bind(this); + this.lastPage = this.lastPage.bind(this); } renderCompletion(completed) { @@ -52,7 +77,7 @@ class TimelineInner extends Component { const { id, completedDate } = completed; const { challengeTitle, challengePath } = idToNameMap.get(id); return ( - + {challengeTitle} @@ -81,9 +106,39 @@ class TimelineInner extends Component { })); } + firstPage() { + this.setState({ + pageNo: 1 + }); + } + nextPage() { + this.setState(state => ({ + pageNo: state.pageNo + 1 + })); + } + + prevPage() { + this.setState(state => ({ + pageNo: state.pageNo - 1 + })); + } + lastPage() { + this.setState((_, props) => ({ + pageNo: props.totalPages + })); + } render() { - const { completedMap, idToNameMap, username } = this.props; - const { solutionToView: id, solutionOpen } = this.state; + const { + completedMap, + idToNameMap, + username, + sortedTimeline, + totalPages = 1 + } = this.props; + const { solutionToView: id, solutionOpen, pageNo = 1 } = this.state; + const startIndex = (pageNo - 1) * ITEMS_PER_PAGE; + const endIndex = pageNo * ITEMS_PER_PAGE; + return (

Timeline

@@ -102,14 +157,9 @@ class TimelineInner extends Component { - {reverse( - sortBy(completedMap, ['completedDate']).filter(challenge => { - return ( - challenge.challengeType !== challengeTypes.step && - idToNameMap.has(challenge.id) - ); - }) - ).map(this.renderCompletion)} + {sortedTimeline + .slice(startIndex, endIndex) + .map(this.renderCompletion)} )} @@ -139,12 +189,22 @@ class TimelineInner extends Component { )} + {totalPages > 1 && ( + + )}
); } } -TimelineInner.propTypes = propTypes; +TimelineInner.propTypes = innerPropTypes; function useIdToNameMap() { const { @@ -173,9 +233,32 @@ function useIdToNameMap() { const Timeline = props => { const idToNameMap = useIdToNameMap(); - return ; + const { completedMap } = props; + // Get the sorted timeline along with total page count. + const { sortedTimeline, totalPages } = useMemo(() => { + const sortedTimeline = reverse( + sortBy(completedMap, ['completedDate']).filter(challenge => { + return ( + challenge.challengeType !== challengeTypes.step && + idToNameMap.has(challenge.id) + ); + }) + ); + const totalPages = Math.ceil(sortedTimeline.length / ITEMS_PER_PAGE); + return { sortedTimeline, totalPages }; + }, [completedMap, idToNameMap]); + return ( + + ); }; +Timeline.propTypes = propTypes; + Timeline.displayName = 'Timeline'; export default Timeline; diff --git a/client/src/components/profile/components/TimelinePagination.js b/client/src/components/profile/components/TimelinePagination.js new file mode 100644 index 0000000000..fc680cd29f --- /dev/null +++ b/client/src/components/profile/components/TimelinePagination.js @@ -0,0 +1,86 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const TimelinePagination = props => { + const { pageNo, totalPages, firstPage, prevPage, nextPage, lastPage } = props; + return ( + + ); +}; + +TimelinePagination.propTypes = { + firstPage: PropTypes.func.isRequired, + lastPage: PropTypes.func.isRequired, + nextPage: PropTypes.func.isRequired, + pageNo: PropTypes.number.isRequired, + prevPage: PropTypes.func.isRequired, + totalPages: PropTypes.number.isRequired +}; + +export default TimelinePagination; diff --git a/client/src/components/profile/components/portfolio.css b/client/src/components/profile/components/portfolio.css index 1a91a8bb7a..db4eb9d0ef 100644 --- a/client/src/components/profile/components/portfolio.css +++ b/client/src/components/profile/components/portfolio.css @@ -10,3 +10,29 @@ .media { word-break: break-all; } + +/* Timeline pagination */ +.timeline-pagination_list { + list-style: none; + padding: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.timeline-pagination_list_item { + margin: 0 5px; +} + +.timeline-pagination_list_item > button { + outline: none; +} + +/* Timeline table styles */ +.timeline-row { + height: 60px; +} + +.timeline-row > td { + vertical-align: middle !important; +}