├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .firebaserc
├── .gitignore
├── .postcssrc.js
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build
├── build.js
├── check-versions.js
├── dev-client.js
├── dev-server.js
├── utils.js
├── vue-loader.conf.js
├── webpack.base.conf.js
├── webpack.dev.conf.js
├── webpack.prod.conf.js
└── webpack.test.conf.js
├── config
├── dev.env.js
├── index.js
├── prod.env.js
└── test.env.js
├── database.rules.json
├── firebase.json
├── functions
├── index.js
└── package.json
├── index.html
├── package.json
├── src
├── App.vue
├── assets
│ ├── css
│ │ └── cal-heatmap.css
│ └── img
│ │ ├── btn_fb_signin_disabled.png
│ │ ├── btn_fb_signin_focus.png
│ │ ├── btn_fb_signin_normal.png
│ │ ├── btn_fb_signin_pressed.png
│ │ ├── btn_google_signin_disabled.png
│ │ ├── btn_google_signin_focus.png
│ │ ├── btn_google_signin_normal.png
│ │ ├── btn_google_signin_pressed.png
│ │ ├── done_anim.svg
│ │ ├── inventory-empty.svg
│ │ ├── pomodoro-logo.png
│ │ ├── today-empty.svg
│ │ ├── tomato.png
│ │ ├── tomato.svg
│ │ └── tomatoadd3.png
├── components
│ ├── AppLoader.vue
│ ├── InfoBox.vue
│ ├── InventoryPane.vue
│ ├── InventoryPaneItem.vue
│ ├── OfflineNotice.vue
│ ├── Signin.vue
│ ├── TheHeader.vue
│ ├── TheMap.vue
│ ├── TheNavigation.vue
│ ├── TodayPane.vue
│ └── TodayPaneItem.vue
├── main.js
├── modules
│ └── firebase.js
├── router
│ └── index.js
└── store
│ ├── actions.js
│ ├── getters.js
│ ├── index.js
│ └── mutations.js
├── static
├── .gitkeep
├── gears.svg
├── manifest.json
├── tomato-120.png
├── tomato-144.png
├── tomato-152.png
├── tomato-192.png
├── tomato-384.png
├── tomato-48.png
└── tomato-512.png
└── test
├── InventoryPane.spec.js
├── InventoryPaneItem.spec.js
├── TodayPane.spec.js
└── TodayPaneItem.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", { "modules": false }],
4 | "stage-2"
5 | ],
6 | "plugins": ["transform-runtime"],
7 | "comments": false,
8 | "env": {
9 | "test": {
10 | "presets": [
11 | ["env", { "targets": { "node": 8 }}]
12 | ]
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parser: 'babel-eslint',
6 | parserOptions: {
7 | sourceType: 'module'
8 | },
9 | env: {
10 | browser: true,
11 | jest: true
12 | },
13 | extends: 'airbnb-base',
14 | // required to lint *.vue files
15 | plugins: ['html'],
16 | // check if imports actually resolve
17 | settings: {
18 | 'import/resolver': {
19 | webpack: {
20 | config: 'build/webpack.base.conf.js'
21 | }
22 | }
23 | },
24 | // add your custom rules here
25 | rules: {
26 | // don't require .vue extension when importing
27 | 'import/extensions': [
28 | 'error',
29 | 'always',
30 | {
31 | js: 'never',
32 | vue: 'never'
33 | }
34 | ],
35 | semi: ['error', 'never'],
36 | 'arrow-body-style': ['error', 'as-needed'],
37 | 'comma-dangle': 0,
38 | 'arrow-parens': [1, 'as-needed'],
39 | // allow optionalDependencies
40 | 'import/no-extraneous-dependencies': [
41 | 'error',
42 | {
43 | optionalDependencies: ['test/unit/index.js']
44 | }
45 | ],
46 | // allow debugger during development
47 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "pomodoro-13b15"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | test/unit/coverage
8 | test/e2e/reports
9 | selenium-debug.log
10 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | // to edit target browsers: use "browserlist" field in package.json
6 | "autoprefixer": {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to this project
2 | Contributions to this project are welcome. Please take a moment to read this document, in order to make the contribution process easier for everyone involved.
3 |
4 | ## Bug Reports
5 | - Before opening a bug report, please use the Github issue search to check if the issue has already been reported.
6 | - When you are creating a bug report, please include as many details as possible: expected vs. observed behavior, steps to reproduce, a description of your environment.
7 |
8 | ## Feature Requests
9 | Feature requests are welcome. But do take a moment to find out whether your idea fits with the scope and aims of the project. Please provide as much detail and context as possible.
10 |
11 | ## Pull Requests
12 | Pull requests are very welcome — be they bug fixes, improvements or new features. Before embarking on a significant pull request, please first discuss the change you wish to make with the owners of this repository, by creating an issue.
13 |
14 | Please make sure your PRs:
15 | - Pass all tests (and — in case of new features — add appropriate tests).
16 | - Adhere to the coding conventions used throughout the project.
17 | - Include as few commits as possible, with clear commit messages, avoiding mixing code changes with whitespace cleanup.
18 | - Are limited to a single issue.
19 | - Have a clear title and description.
20 |
21 | :point_up: By submitting work for inclusion to this project, you agree to allow the project owner to license your work under the same license as that used by the project.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017, Yorgos Panzaris
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Pomo d'Oro
2 |
3 | 
4 |
5 | A progressive web app for the [Pomodoro Technique](https://cirillocompany.de/pages/pomodoro-technique), built with Vue 2, Vuex and Firebase.
6 |
7 | [Live app](https://pomodoro-13b15.firebaseapp.com)
8 |
9 | ## Features
10 | - Google and Facebook authentication
11 | - Inventory and Today views
12 | - Github-style calendar heat map view for tracking productivity over time
13 | - Progressive Web App (100/100 Lighthouse score)
14 | - App manifest
15 | - Service worker
16 | - Use of local storage for limited offline functionality
17 | - Follows the Vue.js [Style Guide](https://vuejs.org/v2/style-guide/)
18 | - Tests with vue-test-utils and Jest
19 |
20 | ## Todos
21 | - Add unit tests for vuex mutations and actions
22 | - Add e2e tests
23 | - Implement syncing between local storage and the Firebase database, for full-featured offline use
24 |
25 | ## Build Setup
26 | > Before building and running this app, you will need to set up [Firebase](https://firebase.google.com) and update the config object in src/modules/firebase.js
27 |
28 | ``` bash
29 | # install dependencies
30 | npm install
31 |
32 | # serve with hot reload at localhost:8080
33 | npm run dev
34 |
35 | # build for production with minification
36 | npm run build
37 |
38 | # build for production and view the bundle analyzer report
39 | npm run build --report
40 |
41 | # run tests
42 | npm test
43 |
44 | # run tests in watch mode
45 | npm run watch
46 | ```
47 |
48 | ## License
49 |
50 | MIT
51 |
52 | - Project initiated with the vue-cli [Webpack template](http://vuejs-templates.github.io/webpack/).
53 | - Tomato [icon](http://www.flaticon.com/free-icon/tomato_167283) by Freepik at www.flaticon.com.
54 |
--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
1 | require('./check-versions')()
2 |
3 | process.env.NODE_ENV = 'production'
4 |
5 | var ora = require('ora')
6 | var rm = require('rimraf')
7 | var path = require('path')
8 | var chalk = require('chalk')
9 | var webpack = require('webpack')
10 | var config = require('../config')
11 | var webpackConfig = require('./webpack.prod.conf')
12 |
13 | var spinner = ora('building for production...')
14 | spinner.start()
15 |
16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
17 | if (err) throw err
18 | webpack(webpackConfig, function (err, stats) {
19 | spinner.stop()
20 | if (err) throw err
21 | process.stdout.write(stats.toString({
22 | colors: true,
23 | modules: false,
24 | children: false,
25 | chunks: false,
26 | chunkModules: false
27 | }) + '\n\n')
28 |
29 | console.log(chalk.cyan(' Build complete.\n'))
30 | console.log(chalk.yellow(
31 | ' Tip: built files are meant to be served over an HTTP server.\n' +
32 | ' Opening index.html over file:// won\'t work.\n'
33 | ))
34 | })
35 | })
36 |
--------------------------------------------------------------------------------
/build/check-versions.js:
--------------------------------------------------------------------------------
1 | var chalk = require('chalk')
2 | var semver = require('semver')
3 | var packageConfig = require('../package.json')
4 |
5 | function exec (cmd) {
6 | return require('child_process').execSync(cmd).toString().trim()
7 | }
8 |
9 | var versionRequirements = [
10 | {
11 | name: 'node',
12 | currentVersion: semver.clean(process.version),
13 | versionRequirement: packageConfig.engines.node
14 | },
15 | {
16 | name: 'npm',
17 | currentVersion: exec('npm --version'),
18 | versionRequirement: packageConfig.engines.npm
19 | }
20 | ]
21 |
22 | module.exports = function () {
23 | var warnings = []
24 | for (var i = 0; i < versionRequirements.length; i++) {
25 | var mod = versionRequirements[i]
26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
27 | warnings.push(mod.name + ': ' +
28 | chalk.red(mod.currentVersion) + ' should be ' +
29 | chalk.green(mod.versionRequirement)
30 | )
31 | }
32 | }
33 |
34 | if (warnings.length) {
35 | console.log('')
36 | console.log(chalk.yellow('To use this template, you must update following to modules:'))
37 | console.log()
38 | for (var i = 0; i < warnings.length; i++) {
39 | var warning = warnings[i]
40 | console.log(' ' + warning)
41 | }
42 | console.log()
43 | process.exit(1)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/build/dev-client.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | require('eventsource-polyfill')
3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
4 |
5 | hotClient.subscribe(function (event) {
6 | if (event.action === 'reload') {
7 | window.location.reload()
8 | }
9 | })
10 |
--------------------------------------------------------------------------------
/build/dev-server.js:
--------------------------------------------------------------------------------
1 | require('./check-versions')()
2 |
3 | var config = require('../config')
4 | if (!process.env.NODE_ENV) {
5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
6 | }
7 |
8 | var opn = require('opn')
9 | var path = require('path')
10 | var express = require('express')
11 | var webpack = require('webpack')
12 | var proxyMiddleware = require('http-proxy-middleware')
13 | var webpackConfig = process.env.NODE_ENV === 'testing'
14 | ? require('./webpack.prod.conf')
15 | : require('./webpack.dev.conf')
16 |
17 | // default port where dev server listens for incoming traffic
18 | var port = process.env.PORT || config.dev.port
19 | // automatically open browser, if not set will be false
20 | var autoOpenBrowser = !!config.dev.autoOpenBrowser
21 | // Define HTTP proxies to your custom API backend
22 | // https://github.com/chimurai/http-proxy-middleware
23 | var proxyTable = config.dev.proxyTable
24 |
25 | var app = express()
26 | var compiler = webpack(webpackConfig)
27 |
28 | var devMiddleware = require('webpack-dev-middleware')(compiler, {
29 | publicPath: webpackConfig.output.publicPath,
30 | quiet: true
31 | })
32 |
33 | var hotMiddleware = require('webpack-hot-middleware')(compiler, {
34 | log: () => {}
35 | })
36 | // force page reload when html-webpack-plugin template changes
37 | compiler.plugin('compilation', function (compilation) {
38 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
39 | hotMiddleware.publish({ action: 'reload' })
40 | cb()
41 | })
42 | })
43 |
44 | // proxy api requests
45 | Object.keys(proxyTable).forEach(function (context) {
46 | var options = proxyTable[context]
47 | if (typeof options === 'string') {
48 | options = { target: options }
49 | }
50 | app.use(proxyMiddleware(options.filter || context, options))
51 | })
52 |
53 | // handle fallback for HTML5 history API
54 | app.use(require('connect-history-api-fallback')())
55 |
56 | // serve webpack bundle output
57 | app.use(devMiddleware)
58 |
59 | // enable hot-reload and state-preserving
60 | // compilation error display
61 | app.use(hotMiddleware)
62 |
63 | // serve pure static assets
64 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
65 | app.use(staticPath, express.static('./static'))
66 |
67 | var uri = 'http://localhost:' + port
68 |
69 | var _resolve
70 | var readyPromise = new Promise(resolve => {
71 | _resolve = resolve
72 | })
73 |
74 | console.log('> Starting dev server...')
75 | devMiddleware.waitUntilValid(() => {
76 | console.log('> Listening at ' + uri + '\n')
77 | // when env is testing, don't need open it
78 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
79 | opn(uri)
80 | }
81 | _resolve()
82 | })
83 |
84 | var server = app.listen(port)
85 |
86 | module.exports = {
87 | ready: readyPromise,
88 | close: () => {
89 | server.close()
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/build/utils.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var config = require('../config')
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
4 |
5 | exports.assetsPath = function (_path) {
6 | var assetsSubDirectory = process.env.NODE_ENV === 'production'
7 | ? config.build.assetsSubDirectory
8 | : config.dev.assetsSubDirectory
9 | return path.posix.join(assetsSubDirectory, _path)
10 | }
11 |
12 | exports.cssLoaders = function (options) {
13 | options = options || {}
14 |
15 | var cssLoader = {
16 | loader: 'css-loader',
17 | options: {
18 | minimize: process.env.NODE_ENV === 'production',
19 | sourceMap: options.sourceMap
20 | }
21 | }
22 |
23 | // generate loader string to be used with extract text plugin
24 | function generateLoaders (loader, loaderOptions) {
25 | var loaders = [cssLoader]
26 | if (loader) {
27 | loaders.push({
28 | loader: loader + '-loader',
29 | options: Object.assign({}, loaderOptions, {
30 | sourceMap: options.sourceMap
31 | })
32 | })
33 | }
34 |
35 | // Extract CSS when that option is specified
36 | // (which is the case during production build)
37 | if (options.extract) {
38 | return ExtractTextPlugin.extract({
39 | use: loaders,
40 | fallback: 'vue-style-loader'
41 | })
42 | } else {
43 | return ['vue-style-loader'].concat(loaders)
44 | }
45 | }
46 |
47 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html
48 | return {
49 | css: generateLoaders(),
50 | postcss: generateLoaders(),
51 | less: generateLoaders('less'),
52 | sass: generateLoaders('sass', { indentedSyntax: true }),
53 | scss: generateLoaders('sass'),
54 | stylus: generateLoaders('stylus'),
55 | styl: generateLoaders('stylus')
56 | }
57 | }
58 |
59 | // Generate loaders for standalone style files (outside of .vue)
60 | exports.styleLoaders = function (options) {
61 | var output = []
62 | var loaders = exports.cssLoaders(options)
63 | for (var extension in loaders) {
64 | var loader = loaders[extension]
65 | output.push({
66 | test: new RegExp('\\.' + extension + '$'),
67 | use: loader
68 | })
69 | }
70 | return output
71 | }
72 |
--------------------------------------------------------------------------------
/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils')
2 | var config = require('../config')
3 | var isProduction = process.env.NODE_ENV === 'production'
4 |
5 | module.exports = {
6 | loaders: utils.cssLoaders({
7 | sourceMap: isProduction
8 | ? config.build.productionSourceMap
9 | : config.dev.cssSourceMap,
10 | extract: isProduction
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var utils = require('./utils')
3 | var config = require('../config')
4 | var vueLoaderConfig = require('./vue-loader.conf')
5 |
6 | function resolve (dir) {
7 | return path.join(__dirname, '..', dir)
8 | }
9 |
10 | module.exports = {
11 | entry: {
12 | app: './src/main.js'
13 | },
14 | output: {
15 | path: config.build.assetsRoot,
16 | filename: '[name].js',
17 | publicPath: process.env.NODE_ENV === 'production'
18 | ? config.build.assetsPublicPath
19 | : config.dev.assetsPublicPath
20 | },
21 | resolve: {
22 | extensions: ['.js', '.vue', '.json'],
23 | alias: {
24 | 'vue$': 'vue/dist/vue.esm.js',
25 | '@': resolve('src')
26 | }
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /\.(js|vue)$/,
32 | loader: 'eslint-loader',
33 | enforce: 'pre',
34 | include: [resolve('src'), resolve('test')],
35 | options: {
36 | formatter: require('eslint-friendly-formatter')
37 | }
38 | },
39 | {
40 | test: /\.vue$/,
41 | loader: 'vue-loader',
42 | options: vueLoaderConfig
43 | },
44 | {
45 | test: /\.js$/,
46 | loader: 'babel-loader',
47 | include: [resolve('src'), resolve('test')]
48 | },
49 | {
50 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
51 | loader: 'url-loader',
52 | options: {
53 | limit: 10000,
54 | name: utils.assetsPath('img/[name].[hash:7].[ext]')
55 | }
56 | },
57 | {
58 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
59 | loader: 'url-loader',
60 | options: {
61 | limit: 10000,
62 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
63 | }
64 | }
65 | ]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | var utils = require('./utils')
2 | var webpack = require('webpack')
3 | var config = require('../config')
4 | var merge = require('webpack-merge')
5 | var baseWebpackConfig = require('./webpack.base.conf')
6 | var HtmlWebpackPlugin = require('html-webpack-plugin')
7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
8 |
9 | // add hot-reload related code to entry chunks
10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) {
11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
12 | })
13 |
14 | module.exports = merge(baseWebpackConfig, {
15 | module: {
16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
17 | },
18 | // cheap-module-eval-source-map is faster for development
19 | devtool: '#cheap-module-eval-source-map',
20 | plugins: [
21 | new webpack.DefinePlugin({
22 | 'process.env': config.dev.env
23 | }),
24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
25 | new webpack.HotModuleReplacementPlugin(),
26 | new webpack.NoEmitOnErrorsPlugin(),
27 | // https://github.com/ampedandwired/html-webpack-plugin
28 | new HtmlWebpackPlugin({
29 | filename: 'index.html',
30 | template: 'index.html',
31 | inject: true
32 | }),
33 | new FriendlyErrorsPlugin()
34 | ]
35 | })
36 |
--------------------------------------------------------------------------------
/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | var path = require('path')
2 | var utils = require('./utils')
3 | var webpack = require('webpack')
4 | var config = require('../config')
5 | var merge = require('webpack-merge')
6 | var baseWebpackConfig = require('./webpack.base.conf')
7 | var CopyWebpackPlugin = require('copy-webpack-plugin')
8 | var HtmlWebpackPlugin = require('html-webpack-plugin')
9 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
10 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
11 | var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin')
12 |
13 | var env = process.env.NODE_ENV === 'testing'
14 | ? require('../config/test.env')
15 | : config.build.env
16 |
17 | var webpackConfig = merge(baseWebpackConfig, {
18 | module: {
19 | rules: utils.styleLoaders({
20 | sourceMap: config.build.productionSourceMap,
21 | extract: true
22 | })
23 | },
24 | devtool: config.build.productionSourceMap ? '#source-map' : false,
25 | output: {
26 | path: config.build.assetsRoot,
27 | filename: utils.assetsPath('js/[name].[chunkhash].js'),
28 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
29 | },
30 | plugins: [
31 | // http://vuejs.github.io/vue-loader/en/workflow/production.html
32 | new webpack.DefinePlugin({
33 | 'process.env': env
34 | }),
35 | new webpack.optimize.UglifyJsPlugin({
36 | compress: {
37 | warnings: false
38 | },
39 | sourceMap: true
40 | }),
41 | // extract css into its own file
42 | new ExtractTextPlugin({
43 | filename: utils.assetsPath('css/[name].[contenthash].css')
44 | }),
45 | // Compress extracted CSS. We are using this plugin so that possible
46 | // duplicated CSS from different components can be deduped.
47 | new OptimizeCSSPlugin({
48 | cssProcessorOptions: {
49 | safe: true
50 | }
51 | }),
52 | // generate dist index.html with correct asset hash for caching.
53 | // you can customize output by editing /index.html
54 | // see https://github.com/ampedandwired/html-webpack-plugin
55 | new HtmlWebpackPlugin({
56 | filename: process.env.NODE_ENV === 'testing'
57 | ? 'index.html'
58 | : config.build.index,
59 | template: 'index.html',
60 | inject: true,
61 | minify: {
62 | removeComments: true,
63 | collapseWhitespace: true,
64 | removeAttributeQuotes: true
65 | // more options:
66 | // https://github.com/kangax/html-minifier#options-quick-reference
67 | },
68 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin
69 | chunksSortMode: 'dependency'
70 | }),
71 | // split vendor js into its own file
72 | new webpack.optimize.CommonsChunkPlugin({
73 | name: 'vendor',
74 | minChunks: function (module, count) {
75 | // any required modules inside node_modules are extracted to vendor
76 | return (
77 | module.resource &&
78 | /\.js$/.test(module.resource) &&
79 | module.resource.indexOf(
80 | path.join(__dirname, '../node_modules')
81 | ) === 0
82 | )
83 | }
84 | }),
85 | // extract webpack runtime and module manifest to its own file in order to
86 | // prevent vendor hash from being updated whenever app bundle is updated
87 | new webpack.optimize.CommonsChunkPlugin({
88 | name: 'manifest',
89 | chunks: ['vendor']
90 | }),
91 | // copy custom static assets
92 | new CopyWebpackPlugin([
93 | {
94 | from: path.resolve(__dirname, '../static'),
95 | to: config.build.assetsSubDirectory,
96 | ignore: ['.*']
97 | }
98 | ]),
99 | new SWPrecacheWebpackPlugin({
100 | cacheId: 'my-vue-app',
101 | filename: 'service-worker.js',
102 | staticFileGlobs: ['dist/**/*.{js,html,css}'],
103 | minify: true,
104 | stripPrefix: 'dist/'
105 | })
106 | ]
107 | })
108 |
109 | if (config.build.productionGzip) {
110 | var CompressionWebpackPlugin = require('compression-webpack-plugin')
111 |
112 | webpackConfig.plugins.push(
113 | new CompressionWebpackPlugin({
114 | asset: '[path].gz[query]',
115 | algorithm: 'gzip',
116 | test: new RegExp(
117 | '\\.(' +
118 | config.build.productionGzipExtensions.join('|') +
119 | ')$'
120 | ),
121 | threshold: 10240,
122 | minRatio: 0.8
123 | })
124 | )
125 | }
126 |
127 | if (config.build.bundleAnalyzerReport) {
128 | var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
129 | webpackConfig.plugins.push(new BundleAnalyzerPlugin())
130 | }
131 |
132 | module.exports = webpackConfig
133 |
--------------------------------------------------------------------------------
/build/webpack.test.conf.js:
--------------------------------------------------------------------------------
1 | // This is the webpack config used for unit tests.
2 |
3 | var utils = require('./utils')
4 | var webpack = require('webpack')
5 | var merge = require('webpack-merge')
6 | var baseConfig = require('./webpack.base.conf')
7 |
8 | var webpackConfig = merge(baseConfig, {
9 | // use inline sourcemap for karma-sourcemap-loader
10 | module: {
11 | rules: utils.styleLoaders()
12 | },
13 | devtool: '#inline-source-map',
14 | plugins: [
15 | new webpack.DefinePlugin({
16 | 'process.env': require('../config/test.env')
17 | })
18 | ]
19 | })
20 |
21 | // no need for app entry during tests
22 | delete webpackConfig.entry
23 |
24 | module.exports = webpackConfig
25 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"'
6 | })
7 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | // see http://vuejs-templates.github.io/webpack for documentation.
2 | var path = require('path')
3 |
4 | module.exports = {
5 | build: {
6 | env: require('./prod.env'),
7 | index: path.resolve(__dirname, '../dist/index.html'),
8 | assetsRoot: path.resolve(__dirname, '../dist'),
9 | assetsSubDirectory: 'static',
10 | assetsPublicPath: '/',
11 | productionSourceMap: true,
12 | // Gzip off by default as many popular static hosts such as
13 | // Surge or Netlify already gzip all static assets for you.
14 | // Before setting to `true`, make sure to:
15 | // npm install --save-dev compression-webpack-plugin
16 | productionGzip: false,
17 | productionGzipExtensions: ['js', 'css'],
18 | // Run the build command with an extra argument to
19 | // View the bundle analyzer report after build finishes:
20 | // `npm run build --report`
21 | // Set to `true` or `false` to always turn it on or off
22 | bundleAnalyzerReport: process.env.npm_config_report
23 | },
24 | dev: {
25 | env: require('./dev.env'),
26 | port: 8080,
27 | autoOpenBrowser: false,
28 | assetsSubDirectory: 'static',
29 | assetsPublicPath: '/',
30 | proxyTable: {},
31 | // CSS Sourcemaps off by default because relative paths are "buggy"
32 | // with this option, according to the CSS-Loader README
33 | // (https://github.com/webpack/css-loader#sourcemaps)
34 | // In our experience, they generally work as expected,
35 | // just be aware of this issue when enabling this option.
36 | cssSourceMap: false
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"'
3 | }
4 |
--------------------------------------------------------------------------------
/config/test.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var devEnv = require('./dev.env')
3 |
4 | module.exports = merge(devEnv, {
5 | NODE_ENV: '"testing"'
6 | })
7 |
--------------------------------------------------------------------------------
/database.rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "tasks": {
4 | "$uid": {
5 | ".read": "auth != null && auth.uid === $uid",
6 | ".write": "auth != null && auth.uid === $uid"
7 | }
8 | },
9 | "dailypoms": {
10 | "$uid": {
11 | ".read": "auth != null && auth.uid === $uid",
12 | ".write": "auth != null && auth.uid === $uid",
13 | ".indexOn": "date"
14 | }
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "rules": "database.rules.json"
4 | },
5 | "hosting": {
6 | "public": "dist",
7 | "rewrites": [
8 | {
9 | "source": "**",
10 | "destination": "/index.html"
11 | }
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/functions/index.js:
--------------------------------------------------------------------------------
1 | const functions = require('firebase-functions')
2 | const admin = require('firebase-admin')
3 | const moment = require('moment')
4 |
5 | admin.initializeApp(functions.config().firebase)
6 | const today = moment().format('YYYY-MM-DD')
7 |
8 | exports.updatePomodori = functions.database.ref('/tasks/{userId}/{taskId}')
9 | .onWrite((event) => {
10 | if (!event.data.previous.val()) {
11 | return
12 | }
13 | /* eslint-disable */
14 | const originalPomodori = event.data.previous.child('pomodori').val()
15 | const currentPomodori = event.data.child('pomodori').val()
16 | if (currentPomodori > originalPomodori) {
17 | console.log('A pomodoro has been added')
18 | console.log('Querying to see if a dailypoms entry exists for today...')
19 | admin.database().ref(`dailypoms/${event.params.userId}`).orderByChild('date').equalTo(today).limitToFirst(1).once('value', (snapshot) => {
20 | if (snapshot.exists()) {
21 | console.log('It does! Adding a pomodoro.')
22 | let theKey
23 | snapshot.forEach((childSnapshot) => {
24 | theKey = childSnapshot.key
25 | })
26 | const newDailyPoms = snapshot.child(`${theKey}/pomodori`).val() + 1
27 | return admin.database().ref(`dailypoms/${event.params.userId}/${theKey}`).update({ pomodori: newDailyPoms })
28 | } else {
29 | console.log('it doesn\'t exist')
30 | admin.database().ref(`dailypoms/${event.params.userId}`).push({ date: today, pomodori: 1 })
31 | return
32 | }
33 | })
34 | } else {
35 | console.log('No pomodoro added by user — nothing to do here.')
36 | return
37 | }
38 | })
39 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "functions",
3 | "description": "Cloud Functions for Firebase",
4 | "dependencies": {
5 | "firebase-admin": "^4.1.2",
6 | "firebase-functions": "^0.5",
7 | "moment": "^2.18.1"
8 | },
9 | "private": true
10 | }
11 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Pomo d' Oro
14 |
15 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Signing in...
34 |
35 |
36 |
37 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pomodoro",
3 | "version": "1.0.0",
4 | "description": "A neat web UI for the pomodoro technique",
5 | "author": "Yorgos Panzaris ",
6 | "private": true,
7 | "scripts": {
8 | "dev": "node build/dev-server.js",
9 | "build": "node build/build.js",
10 | "test": "jest",
11 | "watch": "jest --watch",
12 | "lint": "eslint --ext .js,.vue src test/"
13 | },
14 | "dependencies": {
15 | "cal-heatmap": "^3.6.2",
16 | "fastclick": "^1.0.6",
17 | "firebase": "^3.9.0",
18 | "lodash": "^4.17.4",
19 | "moment": "^2.20.1",
20 | "vue": "^2.5.13",
21 | "vue-easy-toast": "^0.2.7",
22 | "vue-router": "^2.8.1",
23 | "vue-spinner": "^1.0.3",
24 | "vuex": "^2.5.0",
25 | "vuex-persistedstate": "^1.4.1"
26 | },
27 | "devDependencies": {
28 | "@vue/test-utils": "^1.0.0-beta.10",
29 | "autoprefixer": "^6.7.2",
30 | "babel-core": "^6.26.0",
31 | "babel-eslint": "^7.2.3",
32 | "babel-jest": "^22.1.0",
33 | "babel-loader": "^6.2.10",
34 | "babel-plugin-istanbul": "^3.1.2",
35 | "babel-plugin-transform-runtime": "^6.22.0",
36 | "babel-polyfill": "^6.26.0",
37 | "babel-preset-env": "^1.6.1",
38 | "babel-preset-stage-2": "^6.24.1",
39 | "babel-register": "^6.26.0",
40 | "chalk": "^1.1.3",
41 | "chromedriver": "^2.34.1",
42 | "connect-history-api-fallback": "^1.5.0",
43 | "copy-webpack-plugin": "^4.3.1",
44 | "cross-env": "^3.1.4",
45 | "cross-spawn": "^5.0.1",
46 | "css-loader": "^0.26.1",
47 | "eslint": "^3.14.1",
48 | "eslint-config-airbnb-base": "^11.3.2",
49 | "eslint-friendly-formatter": "^2.0.7",
50 | "eslint-import-resolver-webpack": "^0.8.4",
51 | "eslint-loader": "^1.9.0",
52 | "eslint-plugin-html": "^2.0.3",
53 | "eslint-plugin-import": "^2.8.0",
54 | "eventsource-polyfill": "^0.9.6",
55 | "express": "^4.16.2",
56 | "extract-text-webpack-plugin": "^2.1.2",
57 | "file-loader": "^0.10.0",
58 | "friendly-errors-webpack-plugin": "^1.1.3",
59 | "function-bind": "^1.1.1",
60 | "html-webpack-plugin": "^2.30.1",
61 | "http-proxy-middleware": "^0.17.3",
62 | "inject-loader": "^2.0.1",
63 | "jest": "^22.1.1",
64 | "jest-serializer-vue": "^0.3.0",
65 | "lolex": "^1.5.2",
66 | "nightwatch": "^0.9.19",
67 | "opn": "^4.0.2",
68 | "optimize-css-assets-webpack-plugin": "^1.3.2",
69 | "ora": "^1.3.0",
70 | "phantomjs-prebuilt": "^2.1.16",
71 | "rimraf": "^2.6.2",
72 | "selenium-server": "^3.8.1",
73 | "semver": "^5.4.1",
74 | "stylus": "^0.54.5",
75 | "stylus-loader": "^3.0.1",
76 | "sw-precache-webpack-plugin": "^0.11.4",
77 | "url-loader": "^0.5.9",
78 | "vue-jest": "^1.4.0",
79 | "vue-loader": "^11.1.4",
80 | "vue-style-loader": "^2.0.0",
81 | "vue-template-compiler": "^2.5.13",
82 | "webpack": "^2.7.0",
83 | "webpack-bundle-analyzer": "^2.9.2",
84 | "webpack-dev-middleware": "^1.12.2",
85 | "webpack-hot-middleware": "^2.21.0",
86 | "webpack-merge": "^2.6.1"
87 | },
88 | "jest": {
89 | "moduleFileExtensions": [
90 | "js",
91 | "json",
92 | "vue"
93 | ],
94 | "transform": {
95 | "^.+\\.js$": "/node_modules/babel-jest",
96 | ".*\\.(vue)$": "/node_modules/vue-jest"
97 | },
98 | "moduleNameMapper": {
99 | "^@/(.*)$": "/src/$1"
100 | },
101 | "transformIgnorePatterns": [
102 | "node_modules/(?!vue-spinner)"
103 | ],
104 | "snapshotSerializers": [
105 | "/node_modules/jest-serializer-vue"
106 | ],
107 | "collectCoverage": false,
108 | "collectCoverageFrom": [
109 | "**/*.{js,vue}",
110 | "!**/node_modules/**"
111 | ],
112 | "coverageReporters": [
113 | "html",
114 | "text-summary"
115 | ],
116 | "mapCoverage": true
117 | },
118 | "engines": {
119 | "node": ">= 4.0.0",
120 | "npm": ">= 3.0.0"
121 | },
122 | "browserslist": [
123 | "> 1%",
124 | "last 2 versions",
125 | "not ie <= 8"
126 | ]
127 | }
128 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
40 |
41 |
42 |
117 |
118 |
206 |
--------------------------------------------------------------------------------
/src/assets/css/cal-heatmap.css:
--------------------------------------------------------------------------------
1 | /* Cal-HeatMap CSS */
2 |
3 | .cal-heatmap-container {
4 | display: block;
5 | margin: auto;
6 | }
7 |
8 | .cal-heatmap-container .graph
9 | {
10 | font-family: "Lucida Grande", Lucida, Verdana, sans-serif;
11 | }
12 |
13 | .cal-heatmap-container .graph-label
14 | {
15 | fill: #999;
16 | font-size: 10px
17 | }
18 |
19 | .cal-heatmap-container .graph, .cal-heatmap-container .graph-legend rect {
20 | shape-rendering: crispedges
21 | }
22 |
23 | .cal-heatmap-container .graph-rect
24 | {
25 | fill: #ededed
26 | }
27 |
28 | .cal-heatmap-container .graph-subdomain-group rect:hover
29 | {
30 | stroke: #000;
31 | stroke-width: 1px
32 | }
33 |
34 | .cal-heatmap-container .subdomain-text {
35 | font-size: 8px;
36 | fill: #999;
37 | pointer-events: none
38 | }
39 |
40 | .cal-heatmap-container .hover_cursor:hover {
41 | cursor: pointer
42 | }
43 |
44 | .cal-heatmap-container .qi {
45 | background-color: #999;
46 | fill: #999
47 | }
48 |
49 | /*
50 | Remove comment to apply this style to date with value equal to 0
51 | .q0
52 | {
53 | background-color: #fff;
54 | fill: #fff;
55 | stroke: #ededed
56 | }
57 | */
58 |
59 | .cal-heatmap-container .q1
60 | {
61 | background-color: #dae289;
62 | fill: #dae289
63 | }
64 |
65 | .cal-heatmap-container .q2
66 | {
67 | background-color: #cedb9c;
68 | fill: #9cc069
69 | }
70 |
71 | .cal-heatmap-container .q3
72 | {
73 | background-color: #b5cf6b;
74 | fill: #669d45
75 | }
76 |
77 | .cal-heatmap-container .q4
78 | {
79 | background-color: #637939;
80 | fill: #637939
81 | }
82 |
83 | .cal-heatmap-container .q5
84 | {
85 | background-color: #3b6427;
86 | fill: #3b6427
87 | }
88 |
89 | .cal-heatmap-container rect.highlight
90 | {
91 | stroke:#444;
92 | stroke-width:1
93 | }
94 |
95 | .cal-heatmap-container text.highlight
96 | {
97 | fill: #444
98 | }
99 |
100 | .cal-heatmap-container rect.highlight-now
101 | {
102 | stroke: red
103 | }
104 |
105 | .cal-heatmap-container text.highlight-now
106 | {
107 | fill: red;
108 | font-weight: 800
109 | }
110 |
111 | .cal-heatmap-container .domain-background {
112 | fill: none;
113 | shape-rendering: crispedges
114 | }
115 |
116 | .ch-tooltip {
117 | padding: 10px;
118 | background: #222;
119 | color: #bbb;
120 | font-size: 12px;
121 | line-height: 1.4;
122 | width: 140px;
123 | position: absolute;
124 | z-index: 99999;
125 | text-align: center;
126 | border-radius: 2px;
127 | box-shadow: 2px 2px 2px rgba(0,0,0,0.2);
128 | display: none;
129 | box-sizing: border-box;
130 | }
131 |
132 | .ch-tooltip::after{
133 | position: absolute;
134 | width: 0;
135 | height: 0;
136 | border-color: transparent;
137 | border-style: solid;
138 | content: "";
139 | padding: 0;
140 | display: block;
141 | bottom: -6px;
142 | left: 50%;
143 | margin-left: -6px;
144 | border-width: 6px 6px 0;
145 | border-top-color: #222;
146 | }
147 |
--------------------------------------------------------------------------------
/src/assets/img/btn_fb_signin_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/btn_fb_signin_disabled.png
--------------------------------------------------------------------------------
/src/assets/img/btn_fb_signin_focus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/btn_fb_signin_focus.png
--------------------------------------------------------------------------------
/src/assets/img/btn_fb_signin_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/btn_fb_signin_normal.png
--------------------------------------------------------------------------------
/src/assets/img/btn_fb_signin_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/btn_fb_signin_pressed.png
--------------------------------------------------------------------------------
/src/assets/img/btn_google_signin_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/btn_google_signin_disabled.png
--------------------------------------------------------------------------------
/src/assets/img/btn_google_signin_focus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/btn_google_signin_focus.png
--------------------------------------------------------------------------------
/src/assets/img/btn_google_signin_normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/btn_google_signin_normal.png
--------------------------------------------------------------------------------
/src/assets/img/btn_google_signin_pressed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/btn_google_signin_pressed.png
--------------------------------------------------------------------------------
/src/assets/img/done_anim.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | image/svg+xml
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/assets/img/inventory-empty.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
13 |
14 |
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 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/assets/img/pomodoro-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/pomodoro-logo.png
--------------------------------------------------------------------------------
/src/assets/img/today-empty.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
17 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/assets/img/tomato.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/tomato.png
--------------------------------------------------------------------------------
/src/assets/img/tomato.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/assets/img/tomatoadd3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/src/assets/img/tomatoadd3.png
--------------------------------------------------------------------------------
/src/components/AppLoader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
Loading your tasks. Please hold.
10 |
11 |
12 |
13 |
28 |
--------------------------------------------------------------------------------
/src/components/InfoBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | ×
4 | {{ message }}
5 |
6 |
7 |
8 |
24 |
25 |
56 |
--------------------------------------------------------------------------------
/src/components/InventoryPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
You don't have any tasks yet. Use the field below to create some.
30 |
31 |
32 |
33 |
34 |
44 | This field is required
45 |
46 |
47 |
+
48 |
49 |
50 |
51 |
52 |
131 |
132 |
269 |
--------------------------------------------------------------------------------
/src/components/InventoryPaneItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ item.title }}
8 |
9 |
17 | This is required
18 |
19 |
20 |
21 |
22 | {{ item.pomodori }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
90 |
91 |
170 |
--------------------------------------------------------------------------------
/src/components/OfflineNotice.vue:
--------------------------------------------------------------------------------
1 |
2 | No internet connection.
3 |
4 |
5 |
10 |
11 |
44 |
--------------------------------------------------------------------------------
/src/components/Signin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
With Pomo d'Oro, you can track your daily productivity and enforce good work habits, by effectively using the Pomodoro Technique. To begin, you need to sign in with your Google or Facebook account.
6 |
7 |
8 |
You cannot sign in because you are not connected to the internet.
9 |
10 |
16 |
17 |
18 |
19 |
20 |
60 |
61 |
177 |
--------------------------------------------------------------------------------
/src/components/TheHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
15 |
16 |
17 | {{ currentUser.displayName.split(' ')[0] }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
51 |
52 |
53 |
81 |
--------------------------------------------------------------------------------
/src/components/TheMap.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
35 |
36 |
37 |
41 |
63 |
--------------------------------------------------------------------------------
/src/components/TodayPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
22 |
23 |
24 |
You have no tasks on your list for today. Please add some by going to the Inventory tab and clicking the star next the tasks you want to tackle today.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
74 |
75 |
76 |
127 |
--------------------------------------------------------------------------------
/src/components/TodayPaneItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ item.title }}
8 |
9 |
10 |
11 |
12 |
13 |
14 | +{{item.pomodori-3}}
15 |
16 |
17 | {{item.pomodori}}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
61 |
62 |
134 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | import Vue from 'vue'
4 | import Toast from 'vue-easy-toast'
5 | import App from './App'
6 | import router from './router'
7 | import store from './store'
8 | import fbApp from './modules/firebase'
9 |
10 | Vue.use(Toast)
11 | Vue.config.productionTip = false
12 |
13 | /* eslint-disable no-new */
14 |
15 | const unsubscribe = fbApp.auth().onAuthStateChanged(() => {
16 | new Vue({
17 | el: '#app',
18 | router,
19 | store,
20 | render: h => h(App)
21 | })
22 |
23 | unsubscribe()
24 | })
25 |
--------------------------------------------------------------------------------
/src/modules/firebase.js:
--------------------------------------------------------------------------------
1 | import firebase from 'firebase'
2 |
3 | // Initialize Firebase
4 | // Set the configuration for your app
5 | // TODO: Replace with your project's config object
6 | const config = {
7 | apiKey: 'apiKey',
8 | authDomain: 'projectId.firebaseapp.com',
9 | databaseURL: 'https://databaseName.firebaseio.com',
10 | projectId: 'projectId',
11 | storageBucket: 'projectId.appspot.com',
12 | messagingSenderId: 'messagingSenderId'
13 | }
14 |
15 | const fbApp = firebase.initializeApp(config)
16 |
17 | export default fbApp
18 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import InventoryPane from '../components/InventoryPane'
4 | import TodayPane from '../components/TodayPane'
5 | import AppMap from '../components/TheMap'
6 |
7 | Vue.use(Router)
8 |
9 | export default new Router({
10 | mode: 'history',
11 | routes: [
12 | {
13 | path: '/',
14 | name: 'Today',
15 | component: TodayPane
16 | // meta: { requiresAuth: true },
17 | },
18 | {
19 | path: '/inventory',
20 | name: 'Inventory',
21 | component: InventoryPane
22 | // meta: { requiresAuth: true }
23 | },
24 | {
25 | path: '/map',
26 | name: 'Map',
27 | component: AppMap
28 | }
29 | ]
30 | })
31 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import moment from 'moment'
3 | import fbApp from '../modules/firebase'
4 |
5 | const db = fbApp.database()
6 |
7 | const today = moment().format('YYYY-MM-DD') // Is this OK?
8 | const ninetyDaysAgo = moment().subtract(90, 'd').format('YYYY-MM-DD')
9 |
10 | export default {
11 | fetchTasks: ({ commit, state }) => {
12 | db.ref(`tasks/${state.user.uid}`).once('value')
13 | .then(snapshot => commit('SET_TASKS', snapshot.val()), () => commit('TOAST', 'Failed to retrieve tasks'))
14 | },
15 | fetchPomodori: ({ commit, state }) => {
16 | db.ref(`dailypoms/${state.user.uid}`).orderByChild('date').startAt(ninetyDaysAgo).once('value')
17 | .then(snapshot => commit('SET_POMS', snapshot.val()), () => commit('TOAST', 'Failed to retrieve pomodori archive'))
18 | },
19 | toggleDoToday: ({ commit, state }, { taskKey, isToday }) => {
20 | const theDate = isToday ? 0 : today
21 | db.ref(`tasks/${state.user.uid}`).child(taskKey).update({ assignedDate: theDate })
22 | .then(commit('ASSIGN_TODAY', { key: taskKey, date: theDate }), () => commit('TOAST', 'Failed to sync the change'))
23 | },
24 | toggleDone: ({ commit, state }, { taskKey, isDone }) => {
25 | const newStatus = isDone ? 'todo' : 'done'
26 | const completedDate = isDone ? 0 : today
27 | db.ref(`tasks/${state.user.uid}`).child(taskKey).update({ status: newStatus, completedDate })
28 | .then(commit('UPDATE_STATUS', { key: taskKey, status: newStatus, date: completedDate }), () => commit('TOAST', 'Failed to sync the change'))
29 | },
30 | addPomodoro: ({ commit, state, getters }, taskKey) => {
31 | const newPomodori = state.tasks[taskKey].pomodori + 1 || 1
32 | const dailyPomodori = getters.recentPomodori[+new Date(today) / 1000] || 0
33 | db.ref(`tasks/${state.user.uid}`).child(taskKey).update({ pomodori: newPomodori })
34 | .then(() => {
35 | commit('UPDATE_POMODORI', { key: taskKey, pomodori: newPomodori })
36 | /* eslint-disable max-len */
37 | const theKey = (dailyPomodori === 0) ? Math.random().toString(36).slice(2) : _.findKey(state.pomodori, { date: today })
38 | commit('UPDATE_POMS', { key: theKey, pomodori: dailyPomodori + 1 })
39 | }, () => commit('TOAST', 'Failed to sync the change'))
40 | },
41 | addTask: ({ commit, state }, newItem) => {
42 | const newRef = db.ref(`tasks/${state.user.uid}`).push({
43 | title: newItem,
44 | status: 'todo',
45 | pomodori: 0,
46 | assignedDate: 0,
47 | completedDate: 0,
48 | })
49 | newRef.then(commit('ADD_TASK', { newItem, key: newRef.key }), () => commit('TOAST', 'Failed to add task'))
50 | },
51 | editTask: ({ commit, state }, { taskKey, title }) => {
52 | db.ref(`tasks/${state.user.uid}`).child(taskKey).update({ title })
53 | .then(commit('UPDATE_TASK', { key: taskKey, title }), () => commit('TOAST', 'Failed to sync the change'))
54 | },
55 | removeTask: ({ commit, state }, taskKey) => {
56 | db.ref(`tasks/${state.user.uid}`).child(taskKey).remove()
57 | .then(commit('REMOVE_TASK', taskKey), () => commit('TOAST', 'Failed to remove task'))
58 | },
59 | signOut: ({ commit }) => {
60 | fbApp.auth().signOut()
61 | .then(commit('UNSET_USER'), () => commit('TOAST', 'Failed to sign out'))
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import moment from 'moment'
3 |
4 | const today = moment().format('YYYY-MM-DD')
5 |
6 | export default {
7 | getLoadingStatus: state => state.isLoading,
8 | getOnlineStatus: state => state.isOnline,
9 | dismissedInventoryHelp: state => state.dismissedInventoryHelp,
10 | dismissedTodayHelp: state => state.dismissedTodayHelp,
11 | currentUser: state => state.user,
12 | hasCurrentUser: state => !_.isEmpty(state.user),
13 | allTasks: state => state.tasks,
14 | dailyTasks: state => _.pickBy(state.tasks, task => task.assignedDate === today),
15 | openDailyTasks: (state, getters) => _.pickBy(getters.dailyTasks, task => task.status === 'todo'),
16 | numberOfTasksLeftToday: (state, getters) => _.values(getters.openDailyTasks).length,
17 | completedTasks: state => _.pickBy(state.tasks, task => task.status === 'done'),
18 | openTasks: state => _.pickBy(state.tasks, task => task.status === 'todo'),
19 | numberOfOpenTasks: (state, getters) => _.values(getters.openTasks).length,
20 | recentPomodori: state => _.reduce(_.values(state.pomodori), (obj, day) => {
21 | /* eslint-disable no-param-reassign */
22 | obj[+new Date(day.date) / 1000] = day.pomodori
23 | return obj
24 | }, {})
25 | }
26 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import createPersistedState from 'vuex-persistedstate'
4 | import actions from './actions'
5 | import getters from './getters'
6 | import mutations from './mutations'
7 |
8 | Vue.use(Vuex)
9 |
10 | const state = {
11 | isLoading: true,
12 | isOnline: true,
13 | dismissedInventoryHelp: false,
14 | dismissedTodayHelp: false,
15 | user: {},
16 | tasks: {},
17 | pomodori: {}
18 | }
19 |
20 | export default new Vuex.Store({
21 | state,
22 | getters,
23 | mutations,
24 | actions,
25 | plugins: [createPersistedState()]
26 | })
27 |
28 | export { state, getters, mutations, actions }
29 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import moment from 'moment'
3 |
4 | const today = moment().format('YYYY-MM-DD')
5 |
6 | /* eslint-disable no-param-reassign, no-undef, no-console, no-unused-vars */
7 | export default {
8 | SET_USER(state, result) {
9 | state.user = result
10 | },
11 | UNSET_USER(state) {
12 | state.user = {}
13 | // Resetting state for proper local storage sync.
14 | state.tasks = {}
15 | state.pomodori = {}
16 | state.isLoading = true
17 | },
18 | SET_TASKS(state, tasks) {
19 | state.isLoading = false
20 | state.tasks = tasks || {}
21 | },
22 | SET_POMS(state, poms) {
23 | state.pomodori = poms || {}
24 | },
25 | CLEAR_TASKS(state) {
26 | state.tasks = {}
27 | },
28 | ASSIGN_TODAY(state, { key, date }) {
29 | // Vue.set(state.tasks[key], 'assignedDate', date) [use this for non-preexisting properties]
30 | state.tasks[key].assignedDate = date
31 | },
32 | UPDATE_STATUS(state, { key, status, date }) {
33 | // Vue.set(state.tasks[key], 'status', status) [use this for non-preexisting properties]
34 | state.tasks[key].status = status
35 | state.tasks[key].completedDate = date
36 | },
37 | UPDATE_POMODORI(state, { key, pomodori }) {
38 | state.tasks[key].pomodori = pomodori
39 | },
40 | UPDATE_POMS(state, { key, pomodori }) {
41 | if (state.pomodori[key]) {
42 | state.pomodori[key].pomodori = pomodori
43 | } else {
44 | Vue.set(state.pomodori, key, { date: today, pomodori })
45 | }
46 | },
47 | ADD_TASK(state, { newItem, key }) {
48 | Vue.set(state.tasks, key, {
49 | title: newItem,
50 | status: 'todo',
51 | pomodori: 0,
52 | assignedDate: 0,
53 | completedDate: 0
54 | })
55 | },
56 | UPDATE_TASK(state, { key, title }) {
57 | state.tasks[key].title = title
58 | },
59 | REMOVE_TASK(state, key) {
60 | Vue.delete(state.tasks, key)
61 | },
62 | TOAST(state, message) {
63 | state.isLoading = false
64 | // console.log(message) // TODO: log properly (in logger).
65 | },
66 | SET_ONLINE_STATUS(state, status) {
67 | // console.log('Online status provided in mutation:', status)
68 | state.isOnline = status
69 | },
70 | DISMISS_INVENTORY_HELP(state) {
71 | state.dismissedInventoryHelp = true
72 | },
73 | DISMISS_TODAY_HELP(state) {
74 | state.dismissedTodayHelp = true
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/static/.gitkeep
--------------------------------------------------------------------------------
/static/gears.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "The Pomo d'Oro",
3 | "short_name": "Pomo d'Oro",
4 | "icons": [{
5 | "src": "/static/tomato-120.png",
6 | "sizes": "120x120",
7 | "type": "image/png"
8 | }, {
9 | "src": "/static/tomato-144.png",
10 | "sizes": "144x144",
11 | "type": "image/png"
12 | }, {
13 | "src": "/static/tomato-152.png",
14 | "sizes": "152x152",
15 | "type": "image/png"
16 | }, {
17 | "src": "/static/tomato-192.png",
18 | "sizes": "192x192",
19 | "type": "image/png"
20 | },{
21 | "src": "/static/tomato-384.png",
22 | "sizes": "384x384",
23 | "type": "image/png"
24 | },{
25 | "src": "/static/tomato-512.png",
26 | "sizes": "512x512",
27 | "type": "image/png"
28 | }],
29 | "start_url": "/",
30 | "background_color": "#fff",
31 | "display": "standalone",
32 | "theme_color": "#000"
33 | }
34 |
--------------------------------------------------------------------------------
/static/tomato-120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/static/tomato-120.png
--------------------------------------------------------------------------------
/static/tomato-144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/static/tomato-144.png
--------------------------------------------------------------------------------
/static/tomato-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/static/tomato-152.png
--------------------------------------------------------------------------------
/static/tomato-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/static/tomato-192.png
--------------------------------------------------------------------------------
/static/tomato-384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/static/tomato-384.png
--------------------------------------------------------------------------------
/static/tomato-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/static/tomato-48.png
--------------------------------------------------------------------------------
/static/tomato-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/halebopp/vue-pomo/35f4609aceadb8d734a056363d70e6bdabaa1e84/static/tomato-512.png
--------------------------------------------------------------------------------
/test/InventoryPane.spec.js:
--------------------------------------------------------------------------------
1 | import { mount, createLocalVue } from '@vue/test-utils'
2 | import Vuex from 'vuex'
3 | import { state, getters } from '@/store'
4 | import InventoryPane from '@/components/InventoryPane'
5 |
6 | const testItem = { title: 'Make a movie', status: 'todo', pomodori: 0 }
7 |
8 | const localVue = createLocalVue()
9 | localVue.use(Vuex)
10 |
11 | /* eslint-disable no-unused-expressions, no-console, no-undef */
12 | describe('InventoryPane.vue', () => {
13 | let store
14 | let actions
15 | let wrapper
16 |
17 | beforeEach(() => {
18 | actions = {
19 | addTask: jest.fn()
20 | }
21 | store = new Vuex.Store({
22 | state,
23 | getters,
24 | actions
25 | })
26 | wrapper = mount(InventoryPane, { store, localVue })
27 | store.state.tasks = {}
28 | store.state.isLoading = false
29 | })
30 |
31 | it('renders a vue instance', () => {
32 | expect(wrapper.isVueInstance()).toBe(true)
33 | })
34 |
35 | it('displays loading indicator if the content is loading', () => {
36 | store.state.isLoading = true
37 | wrapper.update()
38 | expect(wrapper.contains('.loading-text')).toBe(true)
39 | })
40 |
41 | it('does not display loading indicator if the content is not loading', () => {
42 | expect(wrapper.contains('.loading-text')).toBe(false)
43 | })
44 |
45 | it('displays empty state if no tasks', () => {
46 | expect(wrapper.contains('.empty-state')).toBe(true)
47 | })
48 |
49 | it('does not display empty state if there are tasks', () => {
50 | store.state.tasks = { foo: testItem }
51 | wrapper.update()
52 | expect(wrapper.contains('.empty-state')).toBe(false)
53 | })
54 |
55 | it('displays proper number of tasks', () => {
56 | store.state.tasks = { foo: testItem }
57 | wrapper.update()
58 | expect(wrapper.findAll('span.table-inventory li')).toHaveLength(1)
59 | })
60 |
61 | it('triggers error upon submission of an empty new task and does not call the store action "addTask"', () => {
62 | wrapper.vm.newItem = ''
63 | wrapper.vm.addTask()
64 | expect(wrapper.vm.hasErrors).toBe(true)
65 | expect(actions.addTask).not.toHaveBeenCalled()
66 | })
67 |
68 | it('calls store action "addTask" when a non-empty new task is submitted', () => {
69 | wrapper.vm.newItem = 'Take out the garbage'
70 | wrapper.vm.addTask()
71 | expect(actions.addTask).toHaveBeenCalled()
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/test/InventoryPaneItem.spec.js:
--------------------------------------------------------------------------------
1 | import { shallow, createLocalVue } from '@vue/test-utils'
2 | import Vuex from 'vuex'
3 | import { state, getters } from '@/store'
4 | import moment from 'moment'
5 | import InventoryPaneItem from '@/components/InventoryPaneItem'
6 |
7 | const today = moment().format('YYYY-MM-DD')
8 | const testItem = { title: 'Make a movie', status: 'todo', pomodori: 0 }
9 |
10 | const localVue = createLocalVue()
11 | localVue.use(Vuex)
12 |
13 | describe('InventoryPaneItem.vue', () => {
14 | let wrapper
15 | let actions
16 | let store
17 |
18 | beforeEach(() => {
19 | actions = {
20 | removeTask: jest.fn(),
21 | toggleDoToday: jest.fn()
22 | }
23 | store = new Vuex.Store({
24 | state,
25 | getters,
26 | actions
27 | })
28 | testItem.status = 'todo'
29 | testItem.assignedDate = 0
30 | wrapper = shallow(InventoryPaneItem, {
31 | propsData: { item: testItem, itemKey: 'foo' },
32 | store,
33 | localVue
34 | })
35 | wrapper.vm.inEditMode = false
36 | })
37 |
38 | it('renders a vue instance', () => {
39 | expect(wrapper.isVueInstance()).toBe(true)
40 | })
41 |
42 | it('shows an empty star next to a task with no assigned date', () => {
43 | expect(wrapper.contains('.glyphicon-star-empty')).toBe(true)
44 | })
45 |
46 | it('shows a full star next to a task with an assigned date of today', () => {
47 | testItem.assignedDate = today
48 | wrapper.update()
49 | expect(wrapper.contains('.glyphicon-star-empty')).toBe(false)
50 | })
51 |
52 | it('shows an empty star next to a task with an assigned date other than today', () => {
53 | testItem.assignedDate = '2016-01-01'
54 | wrapper.update()
55 | expect(wrapper.contains('.glyphicon-star-empty')).toBe(true)
56 | })
57 |
58 | it('switches to editing mode once the user clicks on the name of the task', () => {
59 | const taskName = wrapper.find('span.task-cell span')
60 | taskName.trigger('click')
61 | wrapper.update()
62 | expect(wrapper.contains('input.edit-task')).toBe(true)
63 | expect(wrapper.vm.inEditMode).toBe(true)
64 | })
65 |
66 | it('calls store action "removeTask" with the correct itemKey when user clicks on the trash button', () => {
67 | const trashButton = wrapper.find('.glyphicon-trash')
68 | trashButton.trigger('click')
69 | expect(actions.removeTask).toHaveBeenCalledWith(
70 | expect.any(Object),
71 | 'foo',
72 | undefined
73 | )
74 | })
75 |
76 | it('calls store action "toggleDoToday" when user clicks on the today star', () => {
77 | const todayButton = wrapper.find('.today-star')
78 | todayButton.trigger('click')
79 | expect(actions.toggleDoToday).toHaveBeenCalledWith(
80 | expect.any(Object),
81 | { isToday: false, taskKey: 'foo' },
82 | undefined
83 | )
84 | })
85 | })
86 |
--------------------------------------------------------------------------------
/test/TodayPane.spec.js:
--------------------------------------------------------------------------------
1 | import { mount, createLocalVue } from '@vue/test-utils'
2 | import Vuex from 'vuex'
3 | import moment from 'moment'
4 | import { state, getters } from '@/store'
5 | import TodayPane from '@/components/TodayPane'
6 |
7 | const today = moment().format('YYYY-MM-DD')
8 | const testItem = { title: 'Make a movie', status: 'todo', pomodori: 0 }
9 |
10 | const localVue = createLocalVue()
11 | localVue.use(Vuex)
12 |
13 | /* eslint-disable no-unused-expressions, no-console, no-undef */
14 | describe('TodayPane.vue', () => {
15 | let store
16 | let wrapper
17 |
18 | beforeEach(() => {
19 | store = new Vuex.Store({
20 | state,
21 | getters
22 | })
23 | wrapper = mount(TodayPane, { store, localVue })
24 | store.state.tasks = {}
25 | store.state.isLoading = false
26 | })
27 |
28 | it('renders a vue instance', () => {
29 | expect(wrapper.isVueInstance()).toBe(true)
30 | })
31 |
32 | it('displays loading indicator if the content is loading', () => {
33 | store.state.isLoading = true
34 | wrapper.update()
35 | expect(wrapper.contains('.loading-text')).toBe(true)
36 | })
37 |
38 | it('does not display loading indicator if the content is not loading', () => {
39 | expect(wrapper.contains('.loading-text')).toBe(false)
40 | })
41 |
42 | it('displays empty state if no tasks assigned for today', () => {
43 | testItem.assignedDate = '2016-01-01'
44 | store.state.tasks = { foo: testItem }
45 | wrapper.update()
46 | expect(wrapper.contains('.empty-state')).toBe(true)
47 | })
48 |
49 | it('does not display empty state if there are tasks assigned for today', () => {
50 | testItem.assignedDate = today
51 | store.state.tasks = { foo: testItem }
52 | wrapper.update()
53 | expect(wrapper.contains('.empty-state')).toBe(false)
54 | })
55 |
56 | it('displays proper number of tasks', () => {
57 | testItem.assignedDate = today
58 | store.state.tasks = { foo: testItem }
59 | wrapper.update()
60 | expect(wrapper.findAll('span.table-inventory li')).toHaveLength(1)
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/test/TodayPaneItem.spec.js:
--------------------------------------------------------------------------------
1 | import { shallow, createLocalVue } from '@vue/test-utils'
2 | import Vuex from 'vuex'
3 | import { state, getters } from '@/store'
4 | import TodayPaneItem from '@/components/TodayPaneItem'
5 |
6 | const testItem = { title: 'Make a movie', status: 'todo', pomodori: 0 }
7 |
8 | const localVue = createLocalVue()
9 | localVue.use(Vuex)
10 |
11 | describe('TodayPaneItem.vue', () => {
12 | let wrapper
13 | let actions
14 | let store
15 |
16 | beforeEach(() => {
17 | actions = {
18 | addPomodoro: jest.fn(),
19 | toggleDone: jest.fn()
20 | }
21 | store = new Vuex.Store({
22 | state,
23 | getters,
24 | actions
25 | })
26 | testItem.status = 'todo'
27 | wrapper = shallow(TodayPaneItem, {
28 | propsData: { item: testItem, itemKey: 'foo' },
29 | store,
30 | localVue
31 | })
32 | })
33 |
34 | it('renders a vue instance', () => {
35 | expect(wrapper.isVueInstance()).toBe(true)
36 | })
37 |
38 | it('displays done items crossed over and greyed out', () => {
39 | testItem.status = 'done'
40 | wrapper.update()
41 | expect(wrapper.contains('.strikeout')).toBe(true)
42 | expect(wrapper.contains('.glyphicon-check')).toBe(true)
43 | })
44 |
45 | it('calls store action "addPomodoro" with the correct itemKey when user clicks on the tomato button', () => {
46 | const tomatoButton = wrapper.find('.pomodoro-icon')
47 | tomatoButton.trigger('click')
48 | expect(actions.addPomodoro).toHaveBeenCalledWith(
49 | expect.any(Object),
50 | 'foo',
51 | undefined
52 | )
53 | })
54 |
55 | it('calls store action "toggleDone" when user clicks on the done checkbox', () => {
56 | const doneButton = wrapper.find('.done-sign')
57 | doneButton.trigger('click')
58 | expect(actions.toggleDone).toHaveBeenCalledWith(
59 | expect.any(Object),
60 | { taskKey: 'foo', isDone: false },
61 | undefined
62 | )
63 | })
64 | })
65 |
--------------------------------------------------------------------------------