This repository has been archived by the owner on Dec 6, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🎉 Leaderboard page: integration (#2)
- Loading branch information
1 parent
cc6382e
commit 08f6a45
Showing
18 changed files
with
625 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import React from 'react'; | ||
import styled from 'styled-components'; | ||
import { CSVLink } from 'react-csv'; | ||
|
||
import Table from './Table'; | ||
import { colors } from './styledComponents'; | ||
import { getUsersPromise } from '../lib/callApi'; | ||
import { formattedNumber, formattedDate } from '../lib/formatting'; | ||
import { basicSort } from '../lib/sortFunctions'; | ||
import logo from '../assets/logo.mapSwipe.banner.png'; | ||
|
||
const MainContainer = styled.div` | ||
padding: 48px 15vw; | ||
@media(max-width: 600px) { | ||
width: 100%; | ||
padding: 32px 10px; | ||
} | ||
`; | ||
|
||
const Img = styled.img` | ||
width: 450px; | ||
@media(max-width: 600px) { | ||
width: 250px; | ||
} | ||
`; | ||
|
||
const P = styled.p` | ||
color: ${colors.grey}; | ||
width: 100%; | ||
@media(max-width: 600px) { | ||
font-size: 12px; | ||
} | ||
`; | ||
|
||
const EmphSpan1 = styled.span`color: ${colors.orange};`; | ||
const EmphSpan2 = styled.span`color: ${colors.lightOrange}`; | ||
const FormContainer = styled.div``; | ||
const Input = styled.input``; | ||
const SubmitButton = styled.button``; | ||
const StyledCSVLink = styled(CSVLink)` | ||
margin-left: 10px; | ||
color: ${colors.grey}; | ||
`; | ||
|
||
const csvHeaders = [ | ||
{ label: 'Mapped Area', key: 'distance' }, | ||
{ label: 'Contributions', key: 'contributions' }, | ||
{ label: 'Username', key: 'username' }, | ||
]; | ||
|
||
class Board extends React.Component { | ||
constructor() { | ||
super(); | ||
this.state = { totalData: [], totalContributions: 0, totalDistance: 0, query: '', isLoading: true }; | ||
getUsersPromise().then(({ data, totalContributions, totalDistance }) => this.setState({ | ||
totalData: data.sort((a, b) => basicSort(a, b, 'distance')), | ||
totalContributions, | ||
totalDistance, | ||
isLoading: false, | ||
})); | ||
} | ||
|
||
handleOnBlur(event) { | ||
this.setState({ query: event.target.value }); | ||
} | ||
|
||
handleKeyUp(event) { | ||
if (event.keyCode === 13) { | ||
event.preventDefault(); | ||
this.setState({ query: event.target.value }, () => { this.runSearch(); }); | ||
} | ||
} | ||
|
||
runSearch() { | ||
const { query } = this.state; | ||
this.setState({ totalData: [], isLoading: true }); | ||
getUsersPromise(query).then(({ data, totalContributions, totalDistance }) => this.setState({ | ||
totalData: data.sort((a, b) => basicSort(a, b, 'distance')), | ||
totalContributions, | ||
totalDistance, | ||
isLoading: false, | ||
})); | ||
} | ||
|
||
sortFunction(accessor, desc = true) { | ||
const { totalData } = this.state; | ||
const data = [...totalData.sort((a, b) => basicSort(a, b, accessor, desc))]; | ||
this.setState({ totalData: data }); | ||
} | ||
|
||
render() { | ||
const { totalData, totalContributions, totalDistance, isLoading } = this.state; | ||
return ( | ||
<MainContainer> | ||
<Img src={logo} alt="MapSwipe logo" /> | ||
<FormContainer> | ||
<Input type="text" onBlur={(e) => { this.handleOnBlur(e); }} onKeyUp={(e) => { this.handleKeyUp(e); }} /> | ||
<SubmitButton onClick={() => { this.runSearch(); }}>Search</SubmitButton> | ||
</FormContainer> | ||
<P> | ||
Thanks for mapping | ||
<EmphSpan1>{formattedNumber(totalContributions)}</EmphSpan1> | ||
square kms and finding | ||
<EmphSpan2>{formattedNumber(totalDistance)}</EmphSpan2> | ||
objects! | ||
</P> | ||
<Table | ||
totalData={totalData} | ||
sortFunction={(accessor, desc) => this.sortFunction(accessor, desc)} | ||
isLoading={isLoading} | ||
/> | ||
<StyledCSVLink | ||
data={totalData} | ||
headers={csvHeaders} | ||
filename={`users_leaderboard_${formattedDate(new Date())}.csv`} | ||
> | ||
Export CSV | ||
</StyledCSVLink> | ||
</MainContainer> | ||
); | ||
} | ||
} | ||
|
||
export default Board; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import React from 'react'; | ||
import styled from 'styled-components'; | ||
import { ColoredSpan, colors } from './styledComponents'; | ||
|
||
|
||
const StyledSpinner = styled.svg` | ||
animation: rotate 2s linear infinite; | ||
margin: 0px 0 -6px 16px; | ||
width: 25px; | ||
height: 25px; | ||
& .path { | ||
stroke: ${colors.blue}; | ||
stroke-linecap: round; | ||
animation: dash 1.5s ease-in-out infinite; | ||
} | ||
@keyframes rotate { | ||
100% { | ||
transform: rotate(360deg); | ||
} | ||
} | ||
@keyframes dash { | ||
0% { | ||
stroke-dasharray: 1, 150; | ||
stroke-dashoffset: 0; | ||
} | ||
50% { | ||
stroke-dasharray: 90, 150; | ||
stroke-dashoffset: -35; | ||
} | ||
100% { | ||
stroke-dasharray: 90, 150; | ||
stroke-dashoffset: -124; | ||
} | ||
} | ||
`; | ||
|
||
const LoadingComponent = () => ( | ||
<div> | ||
<ColoredSpan color="darkGrey">Loading</ColoredSpan> | ||
<StyledSpinner viewBox="0 0 50 50"> | ||
<circle | ||
className="path" | ||
cx="25" | ||
cy="25" | ||
r="20" | ||
fill="none" | ||
strokeWidth="4" | ||
/> | ||
</StyledSpinner> | ||
</div> | ||
); | ||
|
||
|
||
export default LoadingComponent; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
import React from 'react'; | ||
import memoize from 'memoize-one'; | ||
import PropTypes from 'prop-types'; | ||
import DataTable from 'react-data-table-component'; | ||
import styled from 'styled-components'; | ||
|
||
import TableHeader from './TableHeader'; | ||
import LoadingComponent from './LoadingComponent'; | ||
import { ColoredSpan, Icon } from './styledComponents'; | ||
import { formattedNumber } from '../lib/formatting'; | ||
import icon from '../assets/logo.mapSwipe.png'; | ||
|
||
const StyledDataTable = styled(DataTable)`&&&{ | ||
.rdt_TableCell, .rdt_TableCol { | ||
display: flex; | ||
justify-content: center; | ||
align-items: center; | ||
} | ||
.rdt_TableHeadRow .rdt_TableCol:first-child, .rdt_TableRow .rdt_TableCell:first-child { | ||
min-width: 50px; | ||
max-width: 50px; | ||
padding-left: 12px; | ||
} | ||
.sort-arrows { opacity: 0; } | ||
${({ activeHeader }) => ` | ||
#usernameSortArrow { opacity: ${(activeHeader === 'username') ? 1 : 0} } | ||
#contributionsSortArrow { opacity: ${(activeHeader === 'contributions') ? 1 : 0} } | ||
#distanceSortArrow { opacity: ${(activeHeader === 'distance') ? 1 : 0} } | ||
`} | ||
}`; | ||
|
||
const UsernameCell = styled.div` | ||
display: flex; | ||
align-items: center; | ||
width: 100%; | ||
padding-left: 25%; | ||
`; | ||
|
||
const IndexSpan = styled.span` | ||
min-width: 42px; | ||
margin-right: 6%; | ||
font-size: 13px; | ||
text-align: center; | ||
`; | ||
|
||
const StyledColoredSpan = styled(ColoredSpan)`margin-left: -20px`; | ||
|
||
const styledContributionsCell = row => ( | ||
<StyledColoredSpan color="orange">{formattedNumber(row.contributions)}</StyledColoredSpan> | ||
); | ||
|
||
const styledDistanceCell = row => ( | ||
<StyledColoredSpan color="darkGrey">{formattedNumber(row.distance)}</StyledColoredSpan> | ||
); | ||
|
||
const styledIndexCell = row => ( | ||
<ColoredSpan color="blue">{<IndexSpan>{`${row.index}.`}</IndexSpan>}</ColoredSpan> | ||
); | ||
const styledUsernameCell = row => ( | ||
<UsernameCell> | ||
<Icon src={icon} alt={row.username} /> | ||
<ColoredSpan color="blue">{row.username}</ColoredSpan> | ||
</UsernameCell> | ||
); | ||
|
||
|
||
const getRowIndex = (index, page, perPage) => ((page - 1) * perPage) + index + 1; | ||
const getDataForPage = memoize((totalData, page, perPage) => ( | ||
{ | ||
data: totalData.slice((page - 1) * perPage, (page * perPage)) | ||
.map((datum, index) => ({ ...datum, index: getRowIndex(index, page, perPage) })), | ||
totalRows: totalData.length, | ||
} | ||
)); | ||
|
||
class Table extends React.Component { | ||
constructor(props) { | ||
super(props); | ||
const { sortFunction } = props; | ||
this.state = { page: 1, perPage: 10, sortedHeader: 'distance' }; | ||
this.columns = [ | ||
{ cell: styledIndexCell }, | ||
{ | ||
name: ( | ||
<TableHeader | ||
name="Mapped Area" | ||
accessor="distance" | ||
description="Sq Km Checked" | ||
sortFunction={desc => sortFunction('distance', desc)} | ||
setSortedHeader={(name) => { this.setState({ sortedHeader: name }); }} | ||
/> | ||
), | ||
selector: 'distance', | ||
cell: styledDistanceCell, | ||
}, | ||
{ | ||
name: ( | ||
<TableHeader | ||
name="Contributions" | ||
accessor="contributions" | ||
description="Objects Found" | ||
sortFunction={desc => sortFunction('contributions', desc)} | ||
setSortedHeader={(name) => { this.setState({ sortedHeader: name }); }} | ||
/> | ||
), | ||
selector: 'contributions', | ||
cell: styledContributionsCell, | ||
}, | ||
{ | ||
name: ( | ||
<TableHeader | ||
accessor="username" | ||
name="Username" | ||
sortFunction={desc => sortFunction('username', desc)} | ||
setSortedHeader={(name) => { this.setState({ sortedHeader: name }); }} | ||
/> | ||
), | ||
selector: 'username', | ||
cell: styledUsernameCell, | ||
}, | ||
]; | ||
} | ||
|
||
handlePageChange(page) { | ||
this.setState({ page }); | ||
} | ||
|
||
handlePerRowsChange(perPage, page) { | ||
this.setState({ perPage, page }); | ||
} | ||
|
||
/* eslint-disable no-shadow */ | ||
render() { | ||
const { loading, page, perPage, sortedHeader } = this.state; | ||
const { totalData, isLoading } = this.props; | ||
const { data, totalRows } = getDataForPage(totalData, page, perPage); | ||
return ( | ||
<StyledDataTable | ||
activeHeader={sortedHeader} | ||
columns={this.columns} | ||
data={data} | ||
progressPending={loading} | ||
pagination | ||
noHeader | ||
paginationServer | ||
paginationTotalRows={totalRows} | ||
onChangeRowsPerPage={(perPage, page) => { this.handlePerRowsChange(perPage, page); }} | ||
onChangePage={(page) => { this.handlePageChange(page); }} | ||
noDataComponent={isLoading ? <LoadingComponent /> : 'No data available'} | ||
/> | ||
); | ||
} | ||
} | ||
|
||
Table.propTypes = { | ||
totalData: PropTypes.arrayOf(PropTypes.shape({ | ||
contributions: PropTypes.number.isRequired, | ||
distance: PropTypes.number.isRequired, | ||
username: PropTypes.string.isRequired, | ||
})).isRequired, | ||
sortFunction: PropTypes.func.isRequired, | ||
isLoading: PropTypes.bool.isRequired, | ||
}; | ||
|
||
export default Table; |
Oops, something went wrong.