├── .babelrc
├── .gitignore
├── title.jpg
├── homescreen.png
├── app
├── img
│ ├── code.png
│ ├── share.png
│ ├── coffee.png
│ ├── favicon.png
│ ├── launcher-icon-1x.png
│ ├── play.svg
│ └── stop.svg
├── songs
│ └── alarm.mp3
├── js
│ └── app.js
├── manifest.json
├── pomodoro.appcache
├── components
│ ├── Footer
│ │ └── Footer.js
│ └── Pomodoro
│ │ └── Pomodoro.js
├── index.html
└── css
│ └── style.css
├── notifications.png
├── .travis.yml
├── .eslintrc.json
├── .editorconfig
├── CONTRIBUTING.md
├── __tests__
├── mock-dom.js
└── components
│ ├── Footer
│ └── Footer-test.js
│ └── Pomodoro
│ └── Pomodoro-test.js
├── webpack.config.js
├── LICENSE.md
├── package.json
└── README.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/title.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afonsopacifer/react-pomodoro/HEAD/title.jpg
--------------------------------------------------------------------------------
/homescreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afonsopacifer/react-pomodoro/HEAD/homescreen.png
--------------------------------------------------------------------------------
/app/img/code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afonsopacifer/react-pomodoro/HEAD/app/img/code.png
--------------------------------------------------------------------------------
/app/img/share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afonsopacifer/react-pomodoro/HEAD/app/img/share.png
--------------------------------------------------------------------------------
/notifications.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afonsopacifer/react-pomodoro/HEAD/notifications.png
--------------------------------------------------------------------------------
/app/img/coffee.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afonsopacifer/react-pomodoro/HEAD/app/img/coffee.png
--------------------------------------------------------------------------------
/app/img/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afonsopacifer/react-pomodoro/HEAD/app/img/favicon.png
--------------------------------------------------------------------------------
/app/songs/alarm.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afonsopacifer/react-pomodoro/HEAD/app/songs/alarm.mp3
--------------------------------------------------------------------------------
/app/img/launcher-icon-1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/afonsopacifer/react-pomodoro/HEAD/app/img/launcher-icon-1x.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - stable
5 |
6 | before_script:
7 | - npm install -g mocha
8 |
9 | script:
10 | - npm run lint
11 | - npm test
--------------------------------------------------------------------------------
/app/js/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDom from 'react-dom';
3 | import Pomodoro from './../components/Pomodoro/Pomodoro';
4 |
5 | ReactDom.render(, document.getElementById('app'));
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 6,
4 | "sourceType": "module",
5 | "ecmaFeatures": {
6 | "jsx": true
7 | }
8 | },
9 | "rules": {
10 | "semi": 2
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "React Pomodoro",
3 | "icons": [
4 | {
5 | "src": "img/launcher-icon-1x.png",
6 | "sizes": "48x48",
7 | "type": "image/png",
8 | "density": 1.0
9 | }
10 | ],
11 | "start_url": "index.html",
12 | "display": "standalone"
13 | }
14 |
--------------------------------------------------------------------------------
/app/pomodoro.appcache:
--------------------------------------------------------------------------------
1 | CACHE MANIFEST
2 | # 2021-06-02:v2
3 |
4 | CACHE:
5 | /
6 | /index.html
7 | /bundle.js
8 | /css/style.css
9 | /img/code.png
10 | /img/coffee.png
11 | /img/favicon.png
12 | /img/play.svg
13 | /img/share.png
14 | /img/stop.svg
15 | /songs/alarm.mp3
16 |
17 | NETWORK:
18 | *
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # http://editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | charset = utf-8
12 | trim_trailing_whitespace = true
13 | insert_final_newline = true
14 |
--------------------------------------------------------------------------------
/app/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 |
5 | const Footer = () => (
6 |
9 | );
10 |
11 | export default Footer;
12 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | 1. Fork it!
4 | 2. Create your feature branch: `git checkout -b my-new-feature`
5 | 3. Commit your changes: `git commit -m 'Add some feature'`
6 | 4. Push to the branch: `git push origin my-new-feature`
7 |
8 | **After your pull request is merged**, you can safely delete your branch.
9 |
10 | ### [<-- Back](https://github.com/afonsopacifer/react-pomodoro/)
11 |
--------------------------------------------------------------------------------
/__tests__/mock-dom.js:
--------------------------------------------------------------------------------
1 | let jsdom = require('jsdom');
2 |
3 | global.document = jsdom.jsdom('
');
4 | global.window = document.defaultView;
5 |
6 | global.Notification = {
7 | requestPermission: function () {}
8 | };
9 |
10 | global.localStorage = (function() {
11 | var store = {};
12 | return {
13 | getItem: function(key) {
14 | return store[key];
15 | },
16 | setItem: function(key, value) {
17 | store[key] = value.toString();
18 | },
19 | clear: function() {
20 | store = {};
21 | }
22 | };
23 | })();
24 |
25 | global.navigator = {
26 | userAgent: 'node.js'
27 | };
--------------------------------------------------------------------------------
/__tests__/components/Footer/Footer-test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import TestUtils from 'react-addons-test-utils';
6 | import Footer from './../../../app/components/Footer/Footer';
7 |
8 | import { assert } from 'chai';
9 |
10 | describe(' tests of node elements', () => {
11 | var component;
12 |
13 | var wrapper = React.createClass({
14 | render() {
15 | return ;
16 | }
17 | });
18 |
19 | beforeEach(function() {
20 | component = TestUtils.renderIntoDocument(
21 | React.createElement(wrapper)
22 | );
23 | });
24 |
25 | it('the link of github author should be https://github.com/afonsopacifer', () => {
26 | let linkElement = TestUtils.scryRenderedDOMComponentsWithTag(component, 'a');
27 |
28 | assert.equal(linkElement.length, 1);
29 | assert.equal(linkElement[0].getAttribute('href'), 'https://github.com/afonsopacifer', 'the link is incorrect');
30 | });
31 |
32 | });
33 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var debug = process.env.NODE_ENV !== "production";
2 | var webpack = require('webpack');
3 | var path = require('path');
4 |
5 | module.exports = {
6 | context: path.join(__dirname, "app"),
7 | devtool: debug ? "inline-sourcemap" : null,
8 | entry: "./js/app.js",
9 | module: {
10 | loaders: [
11 | {
12 | test: /\.jsx?$/,
13 | exclude: /(node_modules|bower_components)/,
14 | loader: 'babel-loader',
15 | query: {
16 | presets: ['react', 'es2015', 'stage-0'],
17 | plugins: ['react-html-attrs', 'transform-class-properties', 'transform-decorators-legacy'],
18 | }
19 | }
20 | ]
21 | },
22 | output: {
23 | path: __dirname + "/app/",
24 | filename: "bundle.js"
25 | },
26 | plugins: debug ? [] : [
27 | new webpack.DefinePlugin({
28 | 'process.env': {
29 | 'NODE_ENV': JSON.stringify('production')
30 | }
31 | }),
32 | new webpack.optimize.DedupePlugin(),
33 | new webpack.optimize.OccurenceOrderPlugin(),
34 | new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
35 | ],
36 | };
37 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Afonso Pacifer, [afonsopacifer.com](http://afonsopacifer.com/)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/app/img/play.svg:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/app/img/stop.svg:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-pomodoro",
3 | "version": "0.4.1",
4 | "description": "Pomodoro timer for developers.",
5 | "license": "MIT",
6 | "main": "webpack.config.js",
7 | "author": {
8 | "name": "Afonso Pacifer",
9 | "email": "afonsopacifer@live.com",
10 | "url": "http://afonsopacifer.com"
11 | },
12 | "scripts": {
13 | "start": "./node_modules/.bin/webpack-dev-server --content-base app --inline --hot",
14 | "lint": "./node_modules/.bin/eslint app/components/**/*.js",
15 | "build": "NODE_ENV='production' ./node_modules/.bin/webpack",
16 | "deploy": "git push origin `git subtree split --prefix app master`:gh-pages --force",
17 | "test": "mocha --require ./__tests__/mock-dom.js --compilers js:babel-core/register --recursive __tests__ --timeout 15000"
18 | },
19 | "devDependencies": {
20 | "babel": "^6.5.2",
21 | "babel-core": "^6.7.7",
22 | "babel-loader": "^6.2.0",
23 | "babel-plugin-add-module-exports": "^0.2.1",
24 | "babel-plugin-react-html-attrs": "^2.0.0",
25 | "babel-plugin-transform-class-properties": "^6.3.13",
26 | "babel-plugin-transform-decorators-legacy": "^1.3.4",
27 | "babel-preset-es2015": "^6.9.0",
28 | "babel-preset-react": "^6.5.0",
29 | "babel-preset-stage-0": "^6.3.13",
30 | "chai": "^3.5.0",
31 | "enzyme": "^2.3.0",
32 | "eslint": "^2.11.1",
33 | "jsdom": "^9.2.1",
34 | "mocha": "^2.5.3",
35 | "react-addons-test-utils": "^15.1.0",
36 | "webpack": "^1.12.9",
37 | "webpack-dev-server": "^1.14.1"
38 | },
39 | "dependencies": {
40 | "mousetrap": "^1.5.3",
41 | "react": "^15.0.1",
42 | "react-dom": "^15.0.1",
43 | "react-github-corner": "^0.3.0",
44 | "react-title-component": "^1.0.1"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Pomodoro timer
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [React Pomodoro](http://afonsopacifer.github.io/react-pomodoro/)
2 |
3 | [](https://travis-ci.org/afonsopacifer/react-pomodoro)
4 | [](https://github.com/afonsopacifer/react-pomodoro/archive/0.4.1.zip)
5 | [](https://david-dm.org/afonsopacifer/react-pomodoro)
6 | [](https://david-dm.org/afonsopacifer/react-pomodoro#info=devDependencies)
7 |
8 | > Pomodoro timer for developers.
9 |
10 | 
11 |
12 | ## Features
13 |
14 | - **Add to homescreen**
15 | - **Offline support**
16 | - **Times available:**
17 | - Timer for code - 25min
18 | - Timer for social - 5min
19 | - Timer for Coffee - 15min
20 | - **Time display:**
21 | - Page display
22 | - Title display
23 | 
24 | - **Alarms available:**
25 | - Web Notifications
26 | 
27 | - Vibration
28 | - Songs
29 | - **Basic controls:**
30 | - Play button
31 | - Pause button
32 | - **Keyboard Shortcuts**
33 | - Space: Play
34 | - Ctrl/Command + Left: Toggle mode
35 | - Ctrl/Command + Right: Toggle mode
36 |
37 | ## Versioning
38 |
39 | To keep better organization of releases we follow the [Semantic Versioning 2.0.0](http://semver.org/) guidelines.
40 |
41 | ## Run the project locally
42 |
43 | **1 -** Prepare the environment:
44 |
45 | ```sh
46 | $ npm install -g webpack
47 | ```
48 |
49 | **2 -** Clone the project and install the dependencies:
50 |
51 | ```sh
52 | $ git clone https://github.com/afonsopacifer/react-pomodoro.git
53 | $ cd react-pomodoro
54 | $ npm install
55 | ```
56 | **3 -** Run webpack and webpack-dev-server:
57 |
58 | ```sh
59 | $ npm start
60 | ```
61 |
62 | Go to: [localhost:8080](http://localhost:8080/)
63 |
64 | ## Tasks available
65 |
66 | - `$ npm start` - Run webpack and webpack-dev-server
67 | - `$ npm run lint` - ESlint :D
68 | - `$ npm run test` - Run mocha tests
69 | - `$ npm run build` - Generates the bundle.js
70 | - `$ npm run deploy` - Push for gh-pages
71 |
72 |
73 | ## Contributing
74 | Find on our [issues](https://github.com/afonsopacifer/react-pomodoro/issues/) the next steps of the project ;)
75 |
76 | Want to contribute? [Follow these recommendations](https://github.com/afonsopacifer/react-pomodoro/blob/master/CONTRIBUTING.md).
77 |
78 | ## History
79 | See [Releases](https://github.com/afonsopacifer/react-pomodoro/releases) for detailed changelog.
80 |
81 | ## License
82 | [MIT License](https://github.com/afonsopacifer/react-pomodoro/blob/master/LICENSE.md) © [Afonso Pacifer](http://afonsopacifer.com/)
83 |
--------------------------------------------------------------------------------
/__tests__/components/Pomodoro/Pomodoro-test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import ReactDOM from 'react-dom';
5 | import Pomodoro from './../../../app/components/Pomodoro/Pomodoro';
6 | import { mount } from 'enzyme';
7 |
8 | import { assert } from 'chai';
9 |
10 | describe(' tests of default settings', () => {
11 | var component;
12 |
13 | beforeEach(() => {
14 | component = mount();
15 | });
16 |
17 | it('verify the default state are correctly', () => {
18 | assert.isFalse(component.node.state.play);
19 | assert.equal(component.node.state.time, 1500);
20 | assert.equal(component.node.state.timeType, 1500);
21 | assert.equal(component.node.state.title, '25:00 | Pomodoro timer');
22 | });
23 |
24 | it('the options of notification should be unchecked', () => {
25 | assert.isFalse(component.node.refs.audio.checked);
26 | assert.isFalse(component.node.refs.vibrate.checked);
27 | assert.isFalse(component.node.refs.notification.checked);
28 | });
29 |
30 | it('should be two buttons to control the play and pause', () => {
31 | let buttons = component.find('div.pomodoro div.controlsPlay button');
32 |
33 | assert.equal(buttons.length, 2);
34 |
35 | let playButton = buttons.find('.play').node,
36 | stopButton = buttons.find('.stop').node;
37 |
38 | assert.isDefined(playButton);
39 | assert.isDefined(stopButton);
40 | });
41 |
42 | it('should be three buttons to change pomodoro type', () => {
43 | let buttons = component.find('div.pomodoro div.main div.types button');
44 |
45 | assert.equal(buttons.length, 3);
46 |
47 | let codeButton = buttons.find('.code').node,
48 | socialButton = buttons.find('.social').node,
49 | coffeeButton = buttons.find('.coffee').node;
50 |
51 | assert.isDefined(codeButton);
52 | assert.isDefined(socialButton);
53 | assert.isDefined(coffeeButton);
54 |
55 | assert.equal(codeButton.innerHTML, 'Code');
56 | assert.equal(socialButton.innerHTML, 'Social');
57 | assert.equal(coffeeButton.innerHTML, 'Coffee');
58 | });
59 | });
60 |
61 | describe(' tests behavior of buttons', () => {
62 | var component;
63 |
64 | beforeEach(() => {
65 | component = mount();
66 | });
67 |
68 | it('when click on play the state should be changed', () => {
69 | let playButton = component.find('div.pomodoro div.controlsPlay button.play');
70 |
71 | assert.isFalse(component.node.state.play);
72 | playButton.simulate('click');
73 | assert.isTrue(component.node.state.play);
74 | });
75 |
76 | it('when click on social type the states should be changed', () => {
77 | let socialButton = component.find('div.pomodoro div.types button.social');
78 |
79 | socialButton.simulate('click');
80 |
81 | assert.isTrue(component.node.state.play);
82 | assert.equal(component.node.state.time, 300);
83 | assert.equal(component.node.state.timeType, 300);
84 | assert.equal(component.node.state.title, '05:00 | Pomodoro timer');
85 | });
86 |
87 | it('when click on coffee type the states should be changed', () => {
88 | let coffeeButton = component.find('div.pomodoro div.types button.coffee');
89 |
90 | coffeeButton.simulate('click');
91 |
92 | assert.isTrue(component.node.state.play);
93 | assert.equal(component.node.state.time, 900);
94 | assert.equal(component.node.state.timeType, 900);
95 | assert.equal(component.node.state.title, '15:00 | Pomodoro timer');
96 | });
97 | });
98 |
99 | describe(' check if items on localStorage should be exists', () => {
100 | var component;
101 |
102 | beforeEach(() => {
103 | component = mount();
104 | });
105 |
106 | afterEach(() => {
107 | localStorage.clear();
108 | });
109 |
110 | it('after checked the notification input', () => {
111 | let notificationInput = component.find('div.pomodoro div.controlsCheck #notification');
112 |
113 | let item = 'react-pomodoro-notification';
114 |
115 | assert.isUndefined(localStorage.getItem(item));
116 | notificationInput.simulate('change', { target: { checked: true } });
117 | assert.equal(localStorage.getItem(item), 'true');
118 | });
119 |
120 | it('after checked the audio input', () => {
121 | let audioInput = component.find('div.pomodoro div.controlsCheck #audio');
122 |
123 | let item = 'react-pomodoro-audio';
124 |
125 | assert.isUndefined(localStorage.getItem(item));
126 | audioInput.simulate('change', { target: { checked: true } });
127 | assert.equal(localStorage.getItem(item), 'true');
128 | });
129 |
130 | it('after checked the vibrate input', () => {
131 | let audioInput = component.find('div.pomodoro div.controlsCheck #vibrate');
132 |
133 | let item = 'react-pomodoro-vibrate';
134 |
135 | assert.isUndefined(localStorage.getItem(item));
136 | audioInput.simulate('change', { target: { checked: true } });
137 | assert.equal(localStorage.getItem(item), 'true');
138 | });
139 | });
--------------------------------------------------------------------------------
/app/css/style.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,body,#app {
6 | height: 100%;
7 | }
8 |
9 | body {
10 | font-family: 'Roboto', sans-serif;
11 | font-weight: 300;
12 | background-color: #272727;
13 | }
14 |
15 | @media (min-width: 500px) {
16 | .pomodoro {
17 | height: 100%;
18 | display: -webkit-box;
19 | display: -webkit-flex;
20 | display: -ms-flexbox;
21 | display: flex;
22 | -webkit-box-orient: vertical;
23 | -webkit-box-direction: normal;
24 | -webkit-flex-direction: column;
25 | -ms-flex-direction: column;
26 | flex-direction: column;
27 | -webkit-box-pack: center;
28 | -webkit-justify-content: center;
29 | -ms-flex-pack: center;
30 | justify-content: center;
31 | }
32 | }
33 | /* Main section
34 | ------------------------------- */
35 | .main {
36 | margin-bottom: 61px; /* For stick footer */
37 | }
38 |
39 | @media (max-width: 500px) {
40 | .main {
41 | margin: 40px 0;
42 | }
43 | }
44 |
45 | .container {
46 | display: -webkit-box;
47 | display: -webkit-flex;
48 | display: -ms-flexbox;
49 | display: flex;
50 | -webkit-flex-wrap: wrap;
51 | -ms-flex-wrap: wrap;
52 | flex-wrap: wrap;
53 | -webkit-box-pack: center;
54 | -webkit-justify-content: center;
55 | -ms-flex-pack: center;
56 | justify-content: center;
57 | width: 100%;
58 | max-width: 900px;
59 | margin: 0 auto;
60 | padding: 5px;
61 | }
62 |
63 | /* Display */
64 | .display {
65 | margin-bottom: 20px;
66 | }
67 |
68 | .time {
69 | font-size: 72px;
70 | font-weight: 400;
71 | color: #fff;
72 | display: block;
73 | width: 100%;
74 | text-align: center;
75 | }
76 |
77 | .timeType {
78 | display: block;
79 | font-size: 24px;
80 | color: #2BA0A0;
81 | font-weight: 300;
82 | width: 100%;
83 | text-align: center;
84 | }
85 |
86 | /* Time control */
87 | .btn {
88 | border: 0;
89 | background: #2BA0A0;
90 | color: #fff;
91 | padding: 10px 0;
92 | width: 100%;
93 | max-width: 110px;
94 | margin: 1px;
95 | font-weight: 300;
96 | -webkit-transition: background .3s;
97 | transition: background .3s;
98 | }
99 |
100 | .btn:hover {
101 | background: #1D8888;
102 | }
103 |
104 | .btn:active, .btn:focus {
105 | outline:0;
106 | background: #5AAFAF;
107 | }
108 |
109 | .btnIcon {
110 | border: 0;
111 | background-color: transparent;
112 | background-repeat: no-repeat;
113 | background-position: center;
114 | width: 60px;
115 | height: 60px;
116 | background-size: 50px;
117 | -webkit-transition: opacity .3s;
118 | transition: opacity .3s;
119 | }
120 |
121 | .btnIcon:hover {
122 | opacity: .8;
123 | }
124 |
125 | .btnIcon:active, .btnIcon:focus {
126 | outline:0;
127 | opacity: .5;
128 | }
129 |
130 | .play {
131 | background-image: url("../img/play.svg");
132 | }
133 |
134 | .stop {
135 | background-image: url("../img/stop.svg");
136 | }
137 |
138 | /* Bottom section
139 | ------------------------------- */
140 | .bottomBar {
141 | position: absolute;
142 | width: 100%;
143 | bottom: 0;
144 | }
145 |
146 | @media (max-width: 500px) {
147 | .bottomBar {
148 | position: relative;
149 | }
150 | }
151 |
152 | .controls {
153 | background: #2BA0A0;
154 | padding: 2.5px 0;
155 | }
156 |
157 | .controlsLink {
158 | line-height: 40px;
159 | }
160 | .controlsLink a {
161 | color: #fff;
162 | text-decoration: none;
163 | font-size: 14px;
164 | }
165 | .controlsLink a:hover, .controlsLink a:focus {
166 | text-decoration: underline;
167 | }
168 |
169 | .controlsCheck {
170 | margin-left: auto;
171 | display: -webkit-box;
172 | display: -webkit-flex;
173 | display: -ms-flexbox;
174 | display: flex;
175 | -webkit-box-align: center;
176 | -webkit-align-items: center;
177 | -ms-flex-align: center;
178 | align-items: center;
179 | }
180 |
181 | @media (max-width: 500px) {
182 | .controlsCheck {
183 | width: 100%;
184 | -webkit-box-pack: center;
185 | -webkit-justify-content: center;
186 | -ms-flex-pack: center;
187 | justify-content: center;
188 | margin: 20px 0;
189 | -webkit-flex-wrap: wrap;
190 | -ms-flex-wrap: wrap;
191 | flex-wrap: wrap;
192 | }
193 | }
194 |
195 | .check {
196 | display: inline-block;
197 | margin: 0 10px;
198 | -webkit-transition: color .3s;
199 | transition: color .3s;
200 | line-height: 40px;
201 | }
202 |
203 |
204 | .volume {
205 | display: inline-block;
206 | margin: 0 10px;
207 | -webkit-transition: color .3s;
208 | transition: color .3s;
209 | line-height: 40px;
210 | }
211 |
212 | .volume svg {
213 | width: 15px;
214 | fill: #fff;
215 | }
216 |
217 |
218 | @media (max-width: 500px) {
219 | .check {
220 | width: 100%;
221 | margin: 10px;
222 | }
223 | }
224 |
225 | .checkTitle {
226 | color: #fff;
227 | display: inline-block;
228 | margin: 0 5px;
229 | }
230 |
231 | /* Checkbox style and UI feedback */
232 | .check input[type='checkbox'] {
233 | display: none;
234 | }
235 |
236 | .check label {
237 | border: 1px solid #ccc;
238 | height: 15px;
239 | width: 15px;
240 | margin: 0;
241 | padding: 0;
242 | display: -webkit-inline-box;
243 | display: -webkit-inline-flex;
244 | display: -ms-inline-flexbox;
245 | display: inline-flex;
246 | -webkit-box-pack: center;
247 | -webkit-justify-content: center;
248 | -ms-flex-pack: center;
249 | justify-content: center;
250 | -webkit-box-align: center;
251 | -webkit-align-items: center;
252 | -ms-flex-align: center;
253 | align-items: center;
254 | position: relative;
255 | }
256 |
257 | .check label:hover {
258 | cursor: pointer;
259 | }
260 |
261 | .check input[type='checkbox']:checked + label {
262 | border: 1px solid #DDE00B;
263 | }
264 |
265 | .check input[type='checkbox']:checked + label:after {
266 | background-color: #DDE00B;
267 | position: absolute;
268 | display: block;
269 | bottom: 4px;
270 | left: 1px;
271 | width: 10px;
272 | height: 5px;
273 | -webkit-transform: rotate(45deg);
274 | transform: rotate(45deg);
275 | content: "";
276 | }
277 |
278 | .check input[type='checkbox']:checked + label:before {
279 | background-color: #DDE00B;
280 | position: absolute;
281 | display: block;
282 | bottom: 0px;
283 | right: -1px;
284 | width: 5px;
285 | height: 18px;
286 | -webkit-transform: rotate(45deg);
287 | transform: rotate(45deg);
288 | content: "";
289 | }
290 |
291 | .credits {
292 | color: #FFFFFF;
293 | background: #1D8888;
294 | text-align: center;
295 | padding: 10px 0;
296 | font-size: 14px;
297 | font-weight: 300;
298 | }
299 |
300 | .link {
301 | color: #fff;
302 | text-decoration: none;
303 | -webkit-transition: color .3s;
304 | transition: color .3s;
305 | }
306 |
307 | .link:hover {
308 | opacity: .8;
309 | }
310 |
311 | .link:active. .link:focus {
312 | outline:0;
313 | opacity: .5;
314 | }
315 |
316 | /* Pulse heart */
317 | .heart {
318 | margin: 0 5px;
319 | width: 10px;
320 | height: 10px;
321 | background-color: #DDE00B;
322 | position: relative;
323 | display: inline-block;
324 | -webkit-animation: pulse .5s infinite alternate ease-in;
325 | animation: pulse .5s infinite alternate ease-in;
326 | }
327 |
328 | .heart:before {
329 | width: 10px;
330 | height: 10px;
331 | border-radius: 50%;
332 | background-color: #DDE00B;
333 | position: absolute;
334 | top: 0;
335 | left: 50%;
336 | content: "";
337 | }
338 |
339 | .heart:after {
340 | width: 10px;
341 | height: 10px;
342 | border-radius: 50%;
343 | background-color: #DDE00B;
344 | position: absolute;
345 | top: 50%;
346 | left: 0;
347 | content: "";
348 | }
349 |
350 | @-webkit-keyframes pulse {
351 | from {-webkit-transform: rotate(-135deg) scale(1);transform: rotate(-135deg) scale(1);}
352 | to {-webkit-transform: rotate(-135deg) scale(1.2);transform: rotate(-135deg) scale(1.2);}
353 | }
354 |
355 | @keyframes pulse {
356 | from {-webkit-transform: rotate(-135deg) scale(1);transform: rotate(-135deg) scale(1);}
357 | to {-webkit-transform: rotate(-135deg) scale(1.2);transform: rotate(-135deg) scale(1.2);}
358 | }
359 |
--------------------------------------------------------------------------------
/app/components/Pomodoro/Pomodoro.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDom from 'react-dom';
3 |
4 | import Title from 'react-title-component';
5 | import Mousetrap from 'mousetrap';
6 | import GithubCorner from 'react-github-corner';
7 |
8 | import Footer from './../Footer/Footer';
9 |
10 | export default class Pomodoro extends React.Component {
11 |
12 | constructor() {
13 | super();
14 | this.state = {
15 | time: 0,
16 | play: false,
17 | timeType: 0,
18 | volume: this._getLocalStorageVolume(),
19 | title: ''
20 | };
21 | // Bind early, avoid function creation on render loop
22 | this.setTimeForCode = this.setTime.bind(this, 1500);
23 | this.setTimeForSocial = this.setTime.bind(this, 300);
24 | this.setTimeForCoffee = this.setTime.bind(this, 900);
25 | this.reset = this.reset.bind(this);
26 | this.play = this.play.bind(this);
27 | this.elapseTime = this.elapseTime.bind(this);
28 | }
29 |
30 | componentDidMount() {
31 | this.setDefaultTime();
32 | this.startShortcuts();
33 | Notification.requestPermission();
34 | }
35 |
36 | elapseTime() {
37 | if (this.state.time === 0) {
38 | this.reset(0);
39 | this.alert();
40 | }
41 | if (this.state.play === true) {
42 | let newState = this.state.time - 1;
43 | this.setState({time: newState, title: this.getTitle(newState)});
44 | }
45 | }
46 |
47 | format(seconds) {
48 | let m = Math.floor(seconds % 3600 / 60);
49 | let s = Math.floor(seconds % 3600 % 60);
50 | let timeFormated = (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s;
51 | return timeFormated;
52 | }
53 |
54 | getFormatTypes() {
55 | return [
56 | {type: "code", time: 1500},
57 | {type: "social", time: 300},
58 | {type: "coffee", time: 900}
59 | ];
60 | }
61 |
62 | formatType(timeType) {
63 | let timeTypes = this.getFormatTypes();
64 | for(let i=0; i audio.pause(), 1400);
187 | }
188 | // notification
189 | if(this.refs.notification.checked) {
190 | if (this.state.timeType === 1500) {
191 | let notification = new Notification("Relax :)", {
192 | icon: "img/coffee.png",
193 | lang: "en",
194 | body: "Go talk or drink a coffee."
195 | });
196 | } else {
197 | let notification = new Notification("The time is over!", {
198 | icon: "img/code.png",
199 | lang: "en",
200 | body: "Hey, back to code!"
201 | });
202 | }
203 | }
204 | }
205 |
206 | render() {
207 |
208 | return (
209 |
210 |
215 |
216 |
217 |
218 | {/* Main section
219 | ------------------------------- */}
220 |
221 |
222 |
223 | {this.format(this.state.time)}
224 | The {this.formatType(this.state.timeType)} time!
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
{/* main */}
241 |
242 | {/* Bottom section
243 | ------------------------------- */}
244 |
245 |
246 |
247 |
248 |
249 |
252 |
253 |
{/* controlsCheck */}
310 |
311 |
{/* container */}
312 |
{/* controls */}
313 |
314 |
315 |
316 |
{/* bottomBar */}
317 |
318 |
/* bottomBar */
319 | );
320 | }
321 | };
322 |
--------------------------------------------------------------------------------