├── screenshot.png ├── public ├── bell.flac ├── favicon.ico ├── manifest.json └── index.html ├── src ├── components │ ├── Tasks │ │ ├── TaskList │ │ │ ├── context.js │ │ │ ├── styles.css │ │ │ └── index.js │ │ ├── Task │ │ │ ├── styles.css │ │ │ └── index.js │ │ ├── TaskToggle │ │ │ ├── index.js │ │ │ └── styles.css │ │ └── TaskStatusSelect │ │ │ ├── index.js │ │ │ └── styles.css │ ├── ToggleSound.js │ ├── ToggleSound.css │ ├── TypeSelect.js │ ├── TimeDisplay.css │ ├── TypeSelect.css │ ├── Controls.js │ ├── Shortcuts.js │ ├── Controls.css │ ├── TimeDisplay.js │ └── Shortcuts.css ├── helpers.js ├── index.js └── containers │ ├── Pomodoro.css │ └── Pomodoro.js ├── .editorconfig ├── .gitignore ├── README.md └── package.json /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizbatanero/pomodoro-react/HEAD/screenshot.png -------------------------------------------------------------------------------- /public/bell.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizbatanero/pomodoro-react/HEAD/public/bell.flac -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizbatanero/pomodoro-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/components/Tasks/TaskList/context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export default createContext({}); 4 | -------------------------------------------------------------------------------- /src/components/Tasks/Task/styles.css: -------------------------------------------------------------------------------- 1 | .Dragging { 2 | opacity: 0.5; 3 | border: 2px dashed rgba(0, 0, 0, 1); 4 | cursor: grabbing; 5 | background-color: rgba(0, 0, 0, 0.1); 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | quote_type = single 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = false -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | function pad2(num) { 2 | return num > 9 ? num : `0${num}`; 3 | } 4 | 5 | export function formatTime(time) { 6 | const minutes = pad2(Math.floor(time / 60)); 7 | const seconds = pad2(Math.floor(time % 60)); 8 | 9 | return `${minutes}:${seconds}`; 10 | } 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import HTML5Backend from 'react-dnd-html5-backend'; 4 | import { DndProvider } from 'react-dnd'; 5 | import Pomodoro from './containers/Pomodoro'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Pomodoro React", 3 | "name": "Pomodoro App made with React", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#D9534F", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/components/Tasks/TaskToggle/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import './styles.css'; 3 | 4 | const TaskToggle = ({ task, toggleTask }) => ( 5 | 12 | ); 13 | 14 | export default memo(TaskToggle); 15 | -------------------------------------------------------------------------------- /src/components/Tasks/TaskToggle/styles.css: -------------------------------------------------------------------------------- 1 | .ToggleTask { 2 | position: absolute; 3 | width: 27px; 4 | height: 24px; 5 | right: 115px; 6 | top: 25px; 7 | border: 0; 8 | background: transparent; 9 | font-size: 24px; 10 | color: #666; 11 | opacity: 0.5; 12 | transition: all 0.3s; 13 | outline: 0; 14 | cursor: pointer; 15 | padding: 0; 16 | text-align: left; 17 | } 18 | 19 | .ToggleTask.active { 20 | opacity: 1; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ToggleSound.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import './ToggleSound.css'; 3 | 4 | const ToggleSound = ({ sound, toggleSound }) => ( 5 | 12 | ); 13 | 14 | export default memo(ToggleSound); 15 | -------------------------------------------------------------------------------- /src/components/ToggleSound.css: -------------------------------------------------------------------------------- 1 | .ToggleSound { 2 | position: absolute; 3 | width: 27px; 4 | height: 24px; 5 | right: 25px; 6 | top: 25px; 7 | border: 0; 8 | background: transparent; 9 | font-size: 24px; 10 | color: #666; 11 | opacity: .5; 12 | transition: all .3s; 13 | outline: 0; 14 | cursor: pointer; 15 | padding: 0; 16 | text-align: left; 17 | } 18 | 19 | .ToggleSound.active { 20 | opacity: 1; 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pomodoro Timer 2 | :tomato: Pomodoro timer built with React 3 | 4 | ### [Live Demo](https://luizbatanero.github.io/pomodoro-react/) 5 | 6 | ![Screenshot](screenshot.png) 7 | 8 | ## Features 9 | 10 | * Keyboard Shortcuts 11 | * HTML5 Notification 12 | * Audio Notification 13 | 14 | ## Running Locally 15 | 16 | ```sh 17 | npm install 18 | npm start 19 | ``` 20 | 21 | Runs the app in development mode.
22 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 23 | -------------------------------------------------------------------------------- /src/components/TypeSelect.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import './TypeSelect.css'; 3 | 4 | const TypeSelect = ({ types, changeType, selected }) => ( 5 |
6 | {types.map((type, index) => ( 7 | 14 | ))} 15 |
16 | ); 17 | 18 | export default memo(TypeSelect); 19 | -------------------------------------------------------------------------------- /src/components/Tasks/TaskStatusSelect/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import './styles.css'; 3 | 4 | const TypeSelect = ({ types, changeType, selected }) => ( 5 |
6 | {types.map((type, index) => ( 7 | 14 | ))} 15 |
16 | ); 17 | 18 | export default memo(TypeSelect); 19 | -------------------------------------------------------------------------------- /src/components/TimeDisplay.css: -------------------------------------------------------------------------------- 1 | .TimeDisplay { 2 | position: relative; 3 | } 4 | 5 | .TimeDisplay svg { 6 | max-width: 300px; 7 | } 8 | 9 | .TimeDisplay circle { 10 | transition: stroke-dashoffset 1s; 11 | transform: rotate(-90deg); 12 | transform-origin: 50% 50%; 13 | } 14 | 15 | .TimeDisplay > div { 16 | position: absolute; 17 | left: 50%; 18 | top: 50%; 19 | transform: translate(-50%, -32px); 20 | text-align: center; 21 | } 22 | 23 | .TimeDisplay h1 { 24 | font-size: 52px; 25 | font-weight: 300; 26 | color: #D9534F; 27 | letter-spacing: 2px; 28 | margin: 0; 29 | } 30 | 31 | .TimeDisplay p { 32 | font-size: 14px; 33 | text-transform: uppercase; 34 | color: #bbb; 35 | letter-spacing: 3px; 36 | margin: 10px 0 0; 37 | } 38 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Pomodoro 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/TypeSelect.css: -------------------------------------------------------------------------------- 1 | .TypeSelect { 2 | width: 400px; 3 | display: flex; 4 | } 5 | 6 | @media screen and (max-width: 500px) { 7 | .TypeSelect { 8 | width: 100%; 9 | } 10 | } 11 | 12 | .TypeSelect button { 13 | flex: 1; 14 | border: 2px solid #ccc; 15 | border-right: 0; 16 | background: transparent; 17 | font-family: 'Roboto', sans-serif; 18 | font-size: 14px; 19 | padding: 10px; 20 | color: #999; 21 | outline: 0; 22 | transition: all .3s; 23 | cursor: pointer; 24 | } 25 | 26 | .TypeSelect button:first-child { 27 | border-radius: 5px 0 0 5px; 28 | } 29 | 30 | .TypeSelect button:last-child { 31 | border-right: 2px solid #ccc; 32 | border-radius: 0 5px 5px 0; 33 | } 34 | 35 | .TypeSelect button.active { 36 | background: #fff; 37 | color: #D9534F; 38 | box-shadow: 0 3px 10px rgba(0, 0, 0, .12); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Tasks/TaskStatusSelect/styles.css: -------------------------------------------------------------------------------- 1 | .TypeSelect { 2 | width: 400px; 3 | display: flex; 4 | } 5 | 6 | @media screen and (max-width: 500px) { 7 | .TypeSelect { 8 | width: 100%; 9 | } 10 | } 11 | 12 | .TypeSelect button { 13 | flex: 1; 14 | border: 2px solid #ccc; 15 | border-right: 0; 16 | background: transparent; 17 | font-family: 'Roboto', sans-serif; 18 | font-size: 14px; 19 | padding: 10px; 20 | color: #999; 21 | outline: 0; 22 | transition: all .3s; 23 | cursor: pointer; 24 | } 25 | 26 | .TypeSelect button:first-child { 27 | border-radius: 5px 0 0 5px; 28 | } 29 | 30 | .TypeSelect button:last-child { 31 | border-right: 2px solid #ccc; 32 | border-radius: 0 5px 5px 0; 33 | } 34 | 35 | .TypeSelect button.active { 36 | background: #fff; 37 | color: #D9534F; 38 | box-shadow: 0 3px 10px rgba(0, 0, 0, .12); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Controls.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import './Controls.css'; 3 | 4 | const Controls = ({ start, reset, pause, status }) => ( 5 |
6 | {!status && ( 7 | 10 | )} 11 | 12 | {status === 'Finished' && ( 13 | 16 | )} 17 | 18 | {(status === 'Paused' || status === 'Running') && ( 19 |
20 | 23 | 29 |
30 | )} 31 |
32 | ); 33 | 34 | export default memo(Controls); 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pomodoro-react", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://luizbatanero.github.io/pomodoro-react", 6 | "dependencies": { 7 | "gh-pages": "^2.0.1", 8 | "immer": "^5.0.0", 9 | "react": "^16.8.6", 10 | "react-dnd": "^9.4.0", 11 | "react-dnd-html5-backend": "^9.4.0", 12 | "react-dom": "^16.8.6", 13 | "react-scripts": "3.0.1" 14 | }, 15 | "scripts": { 16 | "predeploy": "npm run build", 17 | "deploy": "gh-pages -d build", 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/containers/Pomodoro.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | padding: 0 5%; 10 | background: #eee; 11 | font-family: "Roboto", sans-serif; 12 | } 13 | 14 | *, 15 | *:before, 16 | *:after { 17 | box-sizing: border-box; 18 | } 19 | 20 | ::selection { 21 | background: #d9534f; 22 | color: #fff; 23 | } 24 | 25 | .Content { 26 | display: flex; 27 | flex-direction: row; 28 | text-align: -webkit-center; 29 | } 30 | 31 | @media (max-width: 1000px) { 32 | .Content { 33 | flex-direction: column; 34 | } 35 | } 36 | 37 | .Pomodoro { 38 | display: flex; 39 | flex-direction: column; 40 | width: 100%; 41 | height: 100%; 42 | min-height: 600px; 43 | justify-content: space-between; 44 | align-items: center; 45 | padding: 80px 0; 46 | } 47 | 48 | .TaskPainel { 49 | display: flex; 50 | flex-direction: column; 51 | width: 100%; 52 | height: 100%; 53 | min-height: 600px; 54 | justify-content: space-between; 55 | align-items: center; 56 | padding: 80px 0; 57 | } 58 | -------------------------------------------------------------------------------- /src/components/Shortcuts.js: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import './Shortcuts.css'; 3 | 4 | const Shortcuts = () => ( 5 |
6 | 7 |
8 |
9 |
Play / Pause / Resume
10 |
11 | Space 12 |
13 |
14 |
15 |
Reset
16 |
17 | Esc 18 |
19 |
20 |
21 |
Pomodoro
22 |
23 | 1 24 | Num1 25 |
26 |
27 |
28 |
Short Break
29 |
30 | 2 31 | Num2 32 |
33 |
34 |
35 |
Long Break
36 |
37 | 3 38 | Num3 39 |
40 |
41 |
42 |
43 | ); 44 | 45 | export default memo(Shortcuts); 46 | -------------------------------------------------------------------------------- /src/components/Tasks/TaskList/styles.css: -------------------------------------------------------------------------------- 1 | .Tasks { 2 | width: 400px; 3 | height: 300px; 4 | overflow-y: scroll; 5 | } 6 | 7 | .Tasks .Tasks-box { 8 | background: #fff; 9 | border-radius: 5px; 10 | border: 1px solid #ddd; 11 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.07); 12 | transition: opacity 0.3s; 13 | } 14 | 15 | .Task { 16 | width: 400px; 17 | background: #fff; 18 | display: flex; 19 | border-bottom: 1px solid #eee; 20 | padding: 15px; 21 | justify-content: space-between; 22 | align-items: center; 23 | font-family: "Roboto", sans-serif; 24 | font-size: 13px; 25 | color: #666; 26 | cursor: grab; 27 | } 28 | 29 | .Task input { 30 | width: 100%; 31 | font-family: "Roboto", sans-serif; 32 | font-size: 13px; 33 | border: none; 34 | outline: none; 35 | } 36 | .Task span { 37 | display: inline-block; 38 | font-family: monospace; 39 | background-color: #f4f4f4; 40 | border: 1px solid #ccc; 41 | border-radius: 3px; 42 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1), inset 0 0 0 1px #fff; 43 | color: #333; 44 | font-size: 11px; 45 | margin: 0 0.15em; 46 | padding: 0.25em 0.7em; 47 | white-space: nowrap; 48 | width: 70px; 49 | text-align: center; 50 | cursor: pointer; 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Controls.css: -------------------------------------------------------------------------------- 1 | .Controls, .Controls div { 2 | width: 250px; 3 | display: flex; 4 | justify-content: center; 5 | } 6 | 7 | .Controls button { 8 | height: 40px; 9 | border-radius: 40px; 10 | border: 0; 11 | flex: 1; 12 | cursor: pointer; 13 | text-transform: uppercase; 14 | font-family: 'Roboto', sans-serif; 15 | font-weight: bold; 16 | font-size: 14px; 17 | letter-spacing: 1px; 18 | position: relative; 19 | outline: 0; 20 | transition: all .3s; 21 | padding: 0; 22 | } 23 | 24 | .Controls button.start { 25 | background: #D9534F; 26 | color: #fff; 27 | } 28 | 29 | .Controls button.start:hover { 30 | background: #b64441; 31 | } 32 | 33 | .Controls button.reset { 34 | background: transparent; 35 | flex: 0; 36 | margin-right: 25px; 37 | color: #888; 38 | } 39 | 40 | .Controls button.reset:hover { 41 | color: #666; 42 | } 43 | 44 | .Controls button.pause { 45 | background: #eea73c; 46 | color: #fff; 47 | } 48 | 49 | .Controls button.pause:hover { 50 | background: #d3912f; 51 | } 52 | 53 | .Controls button.resume { 54 | background: #91b62d; 55 | color: #fff; 56 | } 57 | 58 | .Controls button.resume:hover { 59 | background: #7d9e23; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/TimeDisplay.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { formatTime } from '../helpers'; 3 | import './TimeDisplay.css'; 4 | 5 | const TimeDisplay = ({ time, status, progress }) => { 6 | document.title = `(${formatTime(time)}) Pomodoro`; 7 | 8 | const radius = 150; 9 | const stroke = 5; 10 | const normalizedRadius = radius - stroke * 2; 11 | const circumference = normalizedRadius * 2 * Math.PI; 12 | const strokeDashoffset = circumference - (progress / 100) * circumference; 13 | 14 | return ( 15 |
16 | 17 | 25 | 35 | 36 |
37 |

{formatTime(time)}

38 |

{status}

39 |
40 |
41 | ); 42 | }; 43 | 44 | export default TimeDisplay; 45 | -------------------------------------------------------------------------------- /src/components/Tasks/Task/index.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useContext } from 'react'; 2 | import { useDrag, useDrop } from 'react-dnd'; 3 | import './styles.css'; 4 | 5 | import TaskContext from '../TaskList/context'; 6 | 7 | export default function Task({ task, index }) { 8 | const ref = useRef(); 9 | const { move, handleStatus } = useContext(TaskContext); 10 | const [{ isDragging }, dragRef] = useDrag({ 11 | item: { type: 'TASK', id: task.id, index }, 12 | collect: monitor => ({ 13 | isDragging: monitor.isDragging() 14 | }) 15 | }); 16 | 17 | const [, dropRef] = useDrop({ 18 | accept: 'TASK', 19 | hover(item, monitor) { 20 | if (item.id === task.id) return; 21 | const dragged = item; 22 | const target = task; 23 | const targetSize = ref.current.getBoundingClientRect(); 24 | const targetCenter = (targetSize.bottom - targetSize.top) / 2; 25 | const draggedOffset = monitor.getClientOffset(); 26 | const draggedTop = draggedOffset.y - targetSize.top; 27 | 28 | if (dragged.order < target.order && draggedTop < targetCenter) return; 29 | if (dragged.order > target.order && draggedTop > targetCenter) return; 30 | 31 | move(item.index, index); 32 | item.index = index; 33 | } 34 | }); 35 | 36 | dragRef(dropRef(ref)); 37 | 38 | return ( 39 |
40 |
{task.title}
41 | handleStatus(task)}> 42 | {task.closed ? 'Open' : 'Close'} 43 | 44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Shortcuts.css: -------------------------------------------------------------------------------- 1 | .Shortcuts { 2 | position: absolute; 3 | width: 27px; 4 | height: 24px; 5 | right: 70px; 6 | top: 25px; 7 | } 8 | 9 | @media screen and (max-width: 768px) { 10 | .Shortcuts { 11 | display: none; 12 | } 13 | } 14 | 15 | .Shortcuts i { 16 | font-size: 24px; 17 | color: #666; 18 | opacity: .5; 19 | transition: all .3s; 20 | } 21 | 22 | .Shortcuts:hover i { 23 | opacity: 1; 24 | } 25 | 26 | .Shortcuts:hover .Shortcuts-box { 27 | opacity: 1; 28 | visibility: visible; 29 | } 30 | 31 | .Shortcuts .Shortcuts-box { 32 | position: absolute; 33 | top: 34px; 34 | right: 0; 35 | width: 300px; 36 | background: #fff; 37 | border-radius: 5px; 38 | border: 1px solid #ddd; 39 | box-shadow: 0 1px 4px rgba(0, 0, 0, .07); 40 | padding: 25px; 41 | opacity: 0; 42 | visibility: hidden; 43 | transition: opacity .3s; 44 | } 45 | 46 | .Shortcuts .Shortcut { 47 | display: flex; 48 | border-bottom: 1px solid #eee; 49 | padding: 10px 0; 50 | justify-content: space-between; 51 | align-items: center; 52 | font-family: 'Roboto', sans-serif; 53 | font-size: 13px; 54 | color: #666; 55 | } 56 | 57 | .Shortcuts .Shortcut div { 58 | white-space: nowrap; 59 | } 60 | 61 | .Shortcuts .Shortcut:first-child { 62 | padding-top: 0; 63 | } 64 | 65 | .Shortcuts .Shortcut:last-child { 66 | border-bottom: 0; 67 | padding-bottom: 0; 68 | } 69 | 70 | .Shortcuts .Shortcut kbd { 71 | display: inline-block; 72 | font-family: monospace; 73 | background-color: #f4f4f4; 74 | border: 1px solid #ccc; 75 | border-radius: 3px; 76 | box-shadow: 0 1px 0 rgba(0,0,0,.1), inset 0 0 0 1px #fff; 77 | color: #333; 78 | font-size: 11px; 79 | margin: 0 .15em; 80 | padding: .25em .7em; 81 | white-space: nowrap; 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Tasks/TaskList/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useState, useEffect } from 'react'; 2 | import produce from 'immer'; 3 | import TaskContext from './context'; 4 | import Task from '../Task'; 5 | import TypeSelect from '../../TypeSelect'; 6 | 7 | import './styles.css'; 8 | 9 | const TaskList = ({ selectedTaskType }) => { 10 | const [input, setInput] = useState(''); 11 | const taskStatus = [ 12 | { name: 'All', value: -1 }, 13 | { name: 'Open', value: false }, 14 | { name: 'Closed', value: true } 15 | ]; 16 | 17 | const [tasks, setTasks] = useState( 18 | JSON.parse(window.localStorage.getItem('pomodoro-react-tasks')) || [] 19 | ); 20 | const [selectedStatus, setSelectedStatus] = useState(taskStatus[0]); 21 | 22 | useEffect(() => { 23 | window.localStorage.setItem('pomodoro-react-tasks', JSON.stringify(tasks)); 24 | }, [tasks]); 25 | 26 | function move(from, to) { 27 | setTasks( 28 | produce(tasks, draft => { 29 | const taskMoved = draft[from]; 30 | draft.splice(from, 1); 31 | draft.splice(to, 0, taskMoved); 32 | }) 33 | ); 34 | } 35 | 36 | function handleStatus(task) { 37 | console.log('task:', task); 38 | setTasks( 39 | produce(tasks, draft => { 40 | const foundIndex = draft.findIndex(item => item.id === task.id); 41 | draft[foundIndex].closed = !draft[foundIndex].closed; 42 | }) 43 | ); 44 | } 45 | 46 | function addTask() { 47 | setTasks( 48 | produce(tasks, draft => { 49 | draft.push({ id: draft.length + 1, title: input, closed: false }); 50 | }) 51 | ); 52 | setInput(''); 53 | } 54 | 55 | return ( 56 | 57 | 62 |
63 |
64 | {tasks.length > 0 ? ( 65 | tasks 66 | .filter( 67 | task => 68 | task.closed === selectedStatus.value || 69 | selectedStatus.value === -1 70 | ) 71 | .map((task, index) => ( 72 | 73 | )) 74 | ) : ( 75 |
No Tasks
76 | )} 77 |
78 |
79 |
80 | setInput(e.target.value)} 83 | placeholder="New Task" 84 | /> 85 | {'Add'} 86 |
87 |
88 | ); 89 | }; 90 | 91 | export default memo(TaskList); 92 | -------------------------------------------------------------------------------- /src/containers/Pomodoro.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import TypeSelect from '../components/TypeSelect'; 3 | import TimeDisplay from '../components/TimeDisplay'; 4 | import Controls from '../components/Controls'; 5 | import Shortcuts from '../components/Shortcuts'; 6 | import ToggleSound from '../components/ToggleSound'; 7 | import ToggleTask from '../components/Tasks/TaskToggle'; 8 | import TaskList from '../components/Tasks/TaskList'; 9 | import './Pomodoro.css'; 10 | 11 | class Pomodoro extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | selectedType: props.types[0], 16 | time: props.types[0].time, 17 | interval: null, 18 | running: false, 19 | sound: 20 | JSON.parse(window.localStorage.getItem('pomodoro-react-sound')) || true, 21 | taskStatus: 22 | JSON.parse(window.localStorage.getItem('pomodoro-react-taskStatus')) || 23 | null 24 | }; 25 | } 26 | 27 | static defaultProps = { 28 | types: [ 29 | { name: 'Pomodoro', time: 1500 }, 30 | { name: 'Short Break', time: 300 }, 31 | { name: 'Long Break', time: 900 } 32 | ] 33 | }; 34 | 35 | componentDidMount() { 36 | document.addEventListener('keyup', this.handleKeyUp); 37 | Notification.requestPermission(); 38 | this.sound = new Audio('bell.flac'); 39 | this.sound.preload = 'auto'; 40 | } 41 | 42 | componentWillUnmount() { 43 | document.removeEventListener('keyup', this.handleKeyUp); 44 | } 45 | 46 | handleKeyUp = event => { 47 | if (event.target.tagName === 'INPUT') return; 48 | if (event.key === ' ') { 49 | this.pauseTimer(); 50 | } else if (event.key === 'Escape') { 51 | this.resetTimer(); 52 | } else if (event.key >= 1 && event.key <= this.props.types.length) { 53 | this.changeType(this.props.types[event.key - 1]); 54 | } 55 | }; 56 | 57 | changeType = type => { 58 | this.resetTimer(); 59 | this.setState({ selectedType: type, time: type.time, running: false }); 60 | }; 61 | 62 | tick = () => { 63 | if (this.state.time <= 1) { 64 | this.stopInterval(); 65 | this.setState({ running: false }); 66 | if (this.state.sound) this.sound.play(); 67 | try { 68 | navigator.serviceWorker.register('service-worker.js').then(sw => { 69 | sw.showNotification(`${this.state.selectedType.name} finished!`); 70 | }); 71 | } catch (e) { 72 | console.log('Notification error', e); 73 | } 74 | } 75 | this.setState(state => ({ time: state.time - 1 })); 76 | }; 77 | 78 | stopInterval = () => { 79 | clearInterval(this.state.interval); 80 | this.setState({ interval: null }); 81 | }; 82 | 83 | startTimer = () => { 84 | this.setState(state => ({ 85 | running: true, 86 | interval: setInterval(this.tick, 1000), 87 | time: state.time > 0 ? state.time : state.selectedType.time 88 | })); 89 | this.sound.pause(); 90 | this.sound.currentTime = 0; 91 | }; 92 | 93 | resetTimer = () => { 94 | this.stopInterval(); 95 | this.setState(state => ({ 96 | time: state.selectedType.time, 97 | running: false 98 | })); 99 | }; 100 | 101 | pauseTimer = () => { 102 | this.state.interval ? this.stopInterval() : this.startTimer(); 103 | }; 104 | 105 | getStatus = () => { 106 | const { time, running, interval } = this.state; 107 | if (time === 0) return 'Finished'; 108 | if (running && !interval) return 'Paused'; 109 | if (running) return 'Running'; 110 | }; 111 | 112 | getProgress = () => { 113 | const current = this.state.time; 114 | const total = this.state.selectedType.time; 115 | return ((total - current) / total) * 100; 116 | }; 117 | 118 | handleToggleSound = () => { 119 | this.setState( 120 | state => ({ 121 | sound: !state.sound 122 | }), 123 | () => { 124 | window.localStorage.setItem('pomodoro-react-sound', this.state.sound); 125 | } 126 | ); 127 | }; 128 | 129 | handleToggleTask = () => { 130 | this.setState( 131 | state => ({ 132 | taskStatus: !state.taskStatus 133 | }), 134 | () => { 135 | window.localStorage.setItem( 136 | 'pomodoro-react-taskStatus', 137 | this.state.taskStatus 138 | ); 139 | } 140 | ); 141 | }; 142 | 143 | render() { 144 | const { time, selectedType, sound, taskStatus } = this.state; 145 | const { types } = this.props; 146 | 147 | return ( 148 |
149 |
150 | 155 | 160 | 166 | 167 | 168 | 169 |
170 | {taskStatus && ( 171 |
172 | 173 |
174 | )} 175 |
176 | ); 177 | } 178 | } 179 | 180 | export default Pomodoro; 181 | --------------------------------------------------------------------------------