├── 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 | 
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 |
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 |
--------------------------------------------------------------------------------