diff options
Diffstat (limited to 'src/pages')
-rw-r--r-- | src/pages/Contribute/Contribute.js | 158 | ||||
-rw-r--r-- | src/pages/Scoreboard/Scoreboard.js | 68 | ||||
-rw-r--r-- | src/pages/Timer/Timer.js | 84 | ||||
-rw-r--r-- | src/pages/Timer/TimerButton/TimerButton.js | 119 |
4 files changed, 429 insertions, 0 deletions
diff --git a/src/pages/Contribute/Contribute.js b/src/pages/Contribute/Contribute.js new file mode 100644 index 0000000..6837f59 --- /dev/null +++ b/src/pages/Contribute/Contribute.js @@ -0,0 +1,158 @@ +import React from 'react'; + +import { + Typography, + Button, + List, + ListItem, + Link, + Avatar, + Divider, + makeStyles, +} from "@material-ui/core"; + +import TrendingUpIcon from '@material-ui/icons/TrendingUp'; +import BugReportIcon from '@material-ui/icons/BugReport'; +import NewReleasesIcon from '@material-ui/icons/NewReleases'; + +import Window from "../../components/Window/Window"; +import ContentSection from "../../components/ContentSection/ContentSection"; + + +const useStyles = makeStyles(theme => ({ + mono: { + padding: theme.spacing(4), + + '& .MuiAvatar-root': { + marginRight: theme.spacing(2), + width: theme.spacing(6), + height: theme.spacing(6), + } + }, +})); + + +const developers = require('../../developers.json'); + + +const Contribute = () => { + const classes = useStyles(); + + return ( + <Window type="mono"> + <div className={classes.mono}> + <ContentSection sectionName="Thank You for using ChronoCube!"> + <p> + ChronoCube is an Open-Source application, and we welcome anyone who desires to help our project! + </p> + <Button + variant="contained" + color="secondary" + startIcon={<TrendingUpIcon />} + href="https://github.com/users/Eug-VS/projects/3" + > + Track our progress + </Button> + </ContentSection> + <ContentSection sectionName="Technology stack"> + <p> We only use modern and most relevant technologies to achieve the best results! </p> + <ul> + <li><Link href="https://www.django-rest-framework.org/"> + Django REST Framework + </Link></li> + <li><Link href="https://reactjs.org/"> + React.js + </Link></li> + <li><Link href="https://material-ui.com/"> + Material UI components + </Link></li> + </ul> + <p> Special thanks to other Open-Source projects which made ChronoCube possible: </p> + <ul> + <li><Link href="https://github.com/bvaughn/react-window"> + react-window + </Link></li> + </ul> + </ContentSection> + <ContentSection sectionName="How can I contribute to the project?"> + <p> Thank You for considering helping our project! </p> + <p> + All the development process is being tracked on + the <Link href="https://github.com/users/Eug-VS/projects/3">KanBan board</Link>. + You can always check it to see what is the current state of the project. + To contribute your code, fork our repository and then open + a <Link href="https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests"> + Pull Request</Link>. We will carefully review and, hopefully, accept it! + If you are unfamiliar with this kind of workflow, we recommend + reading <Link href="https://github.com/features/project-management/">GitHub guidelines</Link>. + </p> + <p> + We always welcome newcomers! If you are unfamiliar with certain technologies or even with the + development in general, it is great time to start learning something new! + Our community will kindly assist every your step, and with us you can easily become + highly-evaluated developer! + </p> + <Button + variant="contained" + color="secondary" + startIcon={<NewReleasesIcon />} + href="https://github.com/Eug-VS/chrono-cube/issues/new" + > + Suggest feature + </Button> + <Button + variant="contained" + color="secondary" + startIcon={<BugReportIcon />} + href="https://github.com/Eug-VS/chrono-cube/issues/new" + > + Report a bug + </Button> + </ContentSection> + <ContentSection sectionName="Developers"> + <List> + { + developers.map(developer => { + const githubUrl = `https://github.com/${developer.username}`; + + return ( + <div key={developer.username}> + <ListItem> + <Link href={githubUrl}> + <Avatar alt={developer.username} src={`${githubUrl}.png`} /> + </Link> + <div> + <Link href={githubUrl}> + {developer.username} + </Link> + <Typography component="div" color="textSecondary"> + {developer.role} + </Typography> + </div> + </ListItem> + <Divider variant="middle" /> + </div> + ) + }) + } + <ListItem> + <Avatar /> + You can be here! + </ListItem> + </List> + <Button + variant="contained" + color="secondary" + size="large" + href="https://github.com/users/Eug-VS/projects/3" + > + Join us! + </Button> + </ContentSection> + </div> + </Window> + ); +}; + + +export default Contribute; diff --git a/src/pages/Scoreboard/Scoreboard.js b/src/pages/Scoreboard/Scoreboard.js new file mode 100644 index 0000000..29d26c8 --- /dev/null +++ b/src/pages/Scoreboard/Scoreboard.js @@ -0,0 +1,68 @@ +import React, { useEffect, useState } from 'react'; + +import { makeStyles } from "@material-ui/core/styles"; + +import { get } from "../../requests"; + +import SmartList from "../../components/SmartList/SmartList"; +import SolutionCard from "../../components/SolutionCard/SolutionCard"; +import Loading from "../../components/Loading/Loading"; +import Window from "../../components/Window/Window"; + + +const useStyles = makeStyles(theme => ({ + cell: { + display: 'flex', + justifyContent: 'center', + padding: theme.spacing(4), + + '& .MuiCard-root': { + width: '30%', + } + } +})); + +const Scoreboard = () => { + const classes = useStyles(); + const [solutions, setSolutions] = useState([]); + + const updateSolutions = () => { + get('scoreboard/').then(response => { + setSolutions(response.data); + }); + }; + + const removeSolution = id => { + updateSolutions(); + }; + + useEffect(() => { + setTimeout(updateSolutions, 300); + }, []); + + const renderItem = ({ index, style }) => { + return ( + <div style={style} className={classes.cell}> + <SolutionCard data={solutions[index]} removeThisCard={removeSolution}/> + </div> + ) + }; + + return ( + <Window type="mono"> + { solutions.length === 0 && + <div className={classes.cell}> + <Loading/> + </div> + } + <SmartList + itemSize={300} + itemCount={solutions.length} + renderItem={renderItem} + /> + </Window> + ) +}; + + +export default Scoreboard; diff --git a/src/pages/Timer/Timer.js b/src/pages/Timer/Timer.js new file mode 100644 index 0000000..d63c661 --- /dev/null +++ b/src/pages/Timer/Timer.js @@ -0,0 +1,84 @@ +import React from 'react'; + +import { post } from '../../requests'; + +import Window from "../../components/Window/Window"; +import ContentSection from "../../components/ContentSection/ContentSection"; +import TimerButton from "./TimerButton/TimerButton"; +import SmartList from "../../components/SmartList/SmartList"; +import SolutionCard from "../../components/SolutionCard/SolutionCard"; + +import { Typography, Button, makeStyles } from "@material-ui/core"; + + +const useStyles = makeStyles(theme => ({ + primary: { + padding: theme.spacing(4), + }, + cell: { + padding: theme.spacing(5), + }, +})); + +const Timer = ({ recentSolutions, setRecentSolutions, setPage }) => { + const classes = useStyles(); + + const user = { + id: null, + username: 'anonymous', + }; + + const registerResult = result => { + const solution = { author_id: user.id, result }; + post('solutions/', solution).then(response => { + setRecentSolutions([response.data].concat(recentSolutions)); + }); + }; + + const handleLearnMore = () => { + setPage('contribute'); + }; + + const removeSolution = (id) => { + setRecentSolutions(recentSolutions.filter((solution => solution.id !== id))); + }; + + const renderItem = ({ index, style }) => { + const solution = recentSolutions[index]; + return ( + <div style={style} className={classes.cell}> + <SolutionCard data={solution} removeThisCard={removeSolution} /> + </div> + ); + }; + + return ( + <> + <Window type="primary"> + <div className={classes.primary}> + <ContentSection sectionName="Welcome to ChronoCube!"> + <p> + ChronoCube is a professional speedcubing timer. + Share your results publicly - let everyone see your progress and + achievements! + Every speedcuber will benefit + from using it - both amateur and professional! + </p> + <Button variant="contained" color="secondary" onClick={handleLearnMore}> Learn more </Button> + </ContentSection> + <TimerButton registerResult={registerResult} /> + </div> + </Window> + <Window type="secondary" name="Recent solutions"> + <SmartList + itemSize={270} + itemCount={recentSolutions.length} + renderItem={renderItem} + /> + </Window> + </> + ); +}; + + +export default Timer; diff --git a/src/pages/Timer/TimerButton/TimerButton.js b/src/pages/Timer/TimerButton/TimerButton.js new file mode 100644 index 0000000..56d3084 --- /dev/null +++ b/src/pages/Timer/TimerButton/TimerButton.js @@ -0,0 +1,119 @@ +import React, { useState, useEffect } from 'react'; + +import { Paper, Typography } from '@material-ui/core'; +import { makeStyles } from "@material-ui/core/styles"; + +const useStyles = makeStyles(theme => ({ + root: { + textAlign: 'center', + padding: theme.spacing(5), + background: theme.palette.primary.main, + marginTop: theme.spacing(20), + }, +})); + +const TimerButton = ({ registerResult }) => { + const classes = useStyles(); + + const SPACE = 32; + const maxCountdown = 15000; + const [time, setTime] = useState('00:00:00'); + const [mode, setMode] = useState('idle'); + const [repeater, setRepeater] = useState(0); + + useEffect(()=> { + clearInterval(repeater); + const timestamp = Date.now(); + + if (mode === 'countdown') setRepeater(setInterval(() => { + const timeDelta = maxCountdown - (Date.now() - timestamp); + if (timeDelta <= 0) setMode('over'); + setTime(convertTimeToString(timeDelta)); + }, 10)); + + if (mode === 'running') setRepeater(setInterval(() => { + setTime(convertTimeToString(Date.now() - timestamp)); + }, 10)); + + if (mode === 'over') { + setTime('00:00:00'); + } + }, [mode]); + + const handleKeyPress = event => { + event.preventDefault(); + if (event.keyCode === SPACE && mode === 'idle' ) setMode('countdown'); + }; + + const handleKeyUp = event => { + clearInterval(repeater); + if (event.keyCode === SPACE) { + if (mode === 'running') { + registerResult(time); + setMode('idle'); + } else if (mode === 'over') { + setMode('idle'); + } else { + setMode('running'); + } + } + }; + + useEffect(() => { + window.addEventListener('keyup', handleKeyUp); + window.addEventListener('keypress', handleKeyPress); + + return () => { + window.removeEventListener('keyup', handleKeyUp); + window.removeEventListener('keypress', handleKeyPress); + }; + }); + + const composeHelperText = () => { + switch (mode) { + case 'running': return '_'; + case 'countdown': return 'Release SPACE to begin'; + case 'over': return 'You are too late!'; + default: return 'Hold SPACE to start countdown'; + } + }; + + const helperColor = () => { + switch (mode) { + case 'running': return 'primary'; + case 'over': return 'secondary'; + default: return 'textSecondary'; + } + }; + + return ( + <Paper elevation={3} className={classes.root}> + <Typography variant="h1"> {time} </Typography> + <Typography variant="h5" color={helperColor()}> + {composeHelperText()} + </Typography> + </Paper> + ); +}; + +const convertTimeToString = timeDelta => { + let resultTime = ''; + + const minute = Math.floor(timeDelta / 60000); + if (minute < 10) resultTime += '0'; + resultTime += minute + ':'; + + let second = Math.floor(timeDelta / 1000); + if (second < 10) resultTime += '0'; + if (second > 59) second %= 60; + resultTime += second + ':'; + + const mill = Math.floor((timeDelta % 1000) / 10); + if (mill < 10) resultTime += '0'; + resultTime += mill; + + return resultTime; +}; + + +export default TimerButton; |