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;
+}