├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── babel.config.js ├── jest.config.js ├── mockups ├── Drawer Menu.png ├── Export Page.png ├── Hours Log.png ├── Project Page.png ├── Projects Page.png └── Settings.png ├── nightwatch.conf.js ├── nightwatch.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── icon-256.ico ├── icon.icns ├── icon.ico ├── icon.png └── index.html ├── screenshot.png ├── server.js ├── src ├── App.vue ├── assets │ ├── nw.png │ └── vue.png ├── components │ ├── FsExample.vue │ ├── HelloWorld.vue │ └── LinkList.vue ├── main.js ├── router │ └── index.js ├── store │ ├── dbConfig.js │ └── index.js └── views │ ├── HoursLog.vue │ ├── ProjectEntry.vue │ ├── Projects.vue │ ├── Settings.vue │ ├── SettingsEdit.vue │ └── TimeEntry.vue ├── tests ├── e2e │ ├── custom-assertions │ │ └── elementCount.js │ └── specs │ │ └── test.js └── unit │ ├── .eslintrc.js │ ├── App.test.js │ ├── __snapshots__ │ └── App.test.js.snap │ ├── components │ ├── FsExample.test.js │ ├── HelloWorld.test.js │ ├── LinkList.test.js │ └── __snapshots__ │ │ ├── FsExample.test.js.snap │ │ ├── HelloWorld.test.js.snap │ │ └── LinkList.test.js.snap │ └── setup.js └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | chrome 84 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # Top-most EditorConfig file 4 | root = true 5 | 6 | # defaults for all files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | # Markdown files uses two trailing spaces to indicate a
16 | [*.{md}] 17 | trim_trailing_whitespace = false 18 | 19 | # 4 space indentation 20 | [*.{sass,scss,css}] 21 | indent_size = 4 22 | 23 | # 2 space indentation 24 | [*.{html,json}] 25 | indent_size = 2 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | ecmaVersion: 8, 6 | sourceType: 'module' 7 | }, 8 | env: { 9 | browser: true, 10 | node: true 11 | }, 12 | globals: { 13 | jsdom: true, 14 | Promise: true, 15 | nw: true, 16 | Vue: true, 17 | Vuex: true, 18 | VueRouter: true, 19 | store: true, 20 | router: true 21 | }, 22 | plugins: [ 23 | 'jest', 24 | 'vue' 25 | ], 26 | extends: [ 27 | 'eslint:recommended', 28 | 'plugin:jest/recommended', 29 | 'plugin:vuejs-accessibility/recommended', 30 | 'tjw-base', 31 | 'tjw-vue' 32 | ], 33 | rules: { 34 | 'no-restricted-syntax': [ 35 | 'error' 36 | ], 37 | 'vuejs-accessibility/label-has-for': [ 38 | 'error', 39 | { 40 | 'components': ['Label'], 41 | 'required': { 42 | 'some': ['nesting', 'id'] 43 | }, 44 | 'allowChildren': false 45 | } 46 | ] 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist/ 4 | /dist-vue/ 5 | 6 | tests/unit/coverage/**/* 7 | /tests/e2e/videos/ 8 | /tests/e2e/screenshots/ 9 | /tests/e2e/reports/ 10 | selenium-debug.log 11 | 12 | # local env files 13 | .env.local 14 | .env.*.local 15 | 16 | # Log files 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Editor directories and files 22 | .idea 23 | .vscode 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - "10" 6 | install: 7 | - npm install 8 | script: 9 | - npm run validate 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # "No Ideologies" Code of Conduct 2 | 3 | The following are the guidelines we expect our community members and maintainers to follow. 4 | 5 | * * * 6 | 7 | ## Terminology and Scope 8 | 9 | **What defines a "maintainer"?** 10 | 11 | * A maintainer is anyone that interacts with the community on behalf of this project. Amount of code written is not a qualifier. A maintainer may include those who solely help in support roles such as in resolving issues, improving documentation, administrating or moderating forums/chatrooms, or any other non-coding specific roles. Maintainers also include those that are responsible for the building and upkeep of the project. 12 | 13 | **What defines a "community member"?** 14 | 15 | * Anyone interacting with this project directly, including maintainers. 16 | 17 | **What is the scope of these guidelines?** 18 | 19 | * These guidelines apply only to this project and forms of communication directly related to it, such as issue trackers, forums, chatrooms, and in person events specific to this project. If a member is violating these guidelines outside of this project or on other platforms, that is beyond our scope and any grievances should be handled on those platforms. 20 | 21 | **Discussing the guidelines:** 22 | 23 | * Discussions around these guidelines, improving, updating, or altering them, is permitted so long as the discussions do not violate any existing guidelines. 24 | 25 | * * * 26 | 27 | ## Guidelines 28 | 29 | ### Guidelines for community members 30 | 31 | This project is technical in nature and not based around any particular non-technical ideology. As such, communication that is based primarily around ideologies unrelated to the technologies used by this repository are not permitted. 32 | 33 | Any discussion or communication that is primarily focused around an ideology, be it about race, gender, politics, religion, or anything else non-technical, is not allowed. Everyone has their own ideological preferences, beliefs, and opinions. We do not seek to marginalize, exclude, or judge anyone for their ideologies. To prevent conflict between those with differing or opposing ideologies, all communication on these subjects are prohibited. Some discussions around these topics may be important, however this project is not the proper channel for these discussions. 34 | 35 | ### Guidelines for maintainers 36 | 37 | * Maintainers must abide by the same rules as all other community members mentioned above. However, in addition, maintainers are held to a higher standard, explained below. 38 | * Maintainers should answer all questions politely. 39 | * If someone is upset or angry about something, it's probably because it's difficult to use, so thank them for bringing it to your attention and address ways to solve the problem. Maintainers should focus on the content of the message, and not on how it was delivered. 40 | * A maintainer should seek to update members when an issue they brought up is resolved. 41 | 42 | * * * 43 | 44 | ## Appropriate response to violations 45 | 46 | How to respond to a community member or maintainer violating a guideline. 47 | 48 | 1. If an issue is created that violates a guideline a maintainer should close and lock the issue, explaining "This issue is in violation of our code of conduct. Please review it before posting again." with a link to this document. 49 | 1. If a member repeatedly violates the guidelines established in this document, they should be politely warned that continuing to violate the rules may result in being banned from the community. This means revoking access and support to interactions relating directly to the project (issue trackers, chatrooms, forums, in person events, etc.). However, they may continue to use the technology in accordance with its license. 50 | 1. If a maintainer is in violation of a guideline, they should be informed of such with a link to this document. If additional actions are required of the maintainer but not taken, then other maintainers should be informed of these inactions. 51 | 1. If a maintainer repeatedly violates the guidelines established in this document, they should be politely warned that continuing to violate the rules may result in being banned from the community. This means revoking access and support to interactions relating directly to the project (issue trackers, chatrooms, forums, in person events, etc.). However, they may continue to use the technology in accordance with its license. In addition, future contributions to this project may be ignored as well. 52 | 53 | * * * 54 | 55 | Based on version 1.0.3 from https://github.com/CodifiedConduct/coc-no-ideologies 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 NW.js Utilities 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hour Tracking App 2 | 3 | This app is to help you track your hours and generate invoices for clients as a freelancer or consultant. 4 | 5 | **Table of Contents** 6 | 7 | 1. [Requirements](#requirements) 8 | 1. [Tech Stack](#tech-stack) 9 | 1. [Schema](#schema) 10 | 1. [Mockups](#mockups) 11 | 1. [Design](#design) 12 | 1. [Roadmap](#roadmap) 13 | 14 | ## Requirements 15 | 16 | * Users can keep track of the hours they work 17 | * Users can select which project (client) they are working on 18 | * Users can see a log of entries when they start the timer and stop the timer (manual entry for MVP) 19 | * Users can set their hourly rate and business info in settings 20 | * Users can generate an invoice for each client 21 | * Users can see an overview of how much time they spent on each project over the last week/month/year 22 | 23 | ## Tech Stack 24 | 25 | * Vue.js 26 | * NW.js (desktop application framework for Node.js) 27 | * Database (haven't chosen one yet) 28 | * Bootstrap 29 | 30 | ## Schema 31 | 32 | ### User 33 | 34 | *This app assumes there is only one user for each install. user information is used for generating client invoices.* 35 | 36 | * firstName 37 | * lastName 38 | * streetAddress 39 | * streetAddress2 40 | * city 41 | * state 42 | * zip 43 | * country 44 | * phone 45 | * email 46 | * billingDetails? -> this needs to be more thought out 47 | 48 | ### TimeEntry 49 | 50 | * start: timestamp 51 | * stop: timestamp 52 | * notes: textfield 53 | * project: FK 54 | 55 | ### Project 56 | 57 | * name: charfield 58 | * description: textfield 59 | * color: charfield 60 | * createdAt: timestamp 61 | * updatedAt: timestamp 62 | 63 | ### Log 64 | 65 | * text: textfield 66 | * createdAt: timestamp 67 | * updatedAt: timestamp 68 | * project: FK 69 | 70 | ## Mockups 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ## Design 80 | 81 | ### Colors 82 | 83 | $dark-blue: #0D3B66 84 | 85 | $off-white: #FAF0CA 86 | 87 | $yellow: #F4D35E 88 | 89 | $orange: #EE964B 90 | 91 | $peach: #F95738 92 | 93 | ## Roadmap 94 | 95 | ### MVP 96 | 97 | * Hours log table 98 | * Add new hours entries 99 | * Create new projects (to associate hours with) 100 | * Settings 101 | 102 | ### Version 1 103 | 104 | * Task tracking under projects 105 | * Exports 106 | * Billing Settings 107 | * Notifications 108 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | process.env.VUE_CLI_BABEL_TARGET_NODE = true; 2 | process.env.VUE_CLI_BABEL_TRANSPILE_MODULES = true; 3 | 4 | module.exports = { 5 | preset: '@vue/cli-plugin-unit-jest', 6 | coverageDirectory: '/tests/unit/coverage', 7 | setupFilesAfterEnv: [ 8 | '/tests/unit/setup.js' 9 | ], 10 | snapshotSerializers: [ 11 | '/node_modules/jest-serializer-vue-tjw' 12 | ], 13 | testMatch: [ 14 | '**/tests/unit/**/*.test.js' 15 | ], 16 | testPathIgnorePatterns: [ 17 | '/tests/e2e', 18 | '/dist', 19 | '/dist-vue' 20 | ] 21 | }; 22 | -------------------------------------------------------------------------------- /mockups/Drawer Menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/mockups/Drawer Menu.png -------------------------------------------------------------------------------- /mockups/Export Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/mockups/Export Page.png -------------------------------------------------------------------------------- /mockups/Hours Log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/mockups/Hours Log.png -------------------------------------------------------------------------------- /mockups/Project Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/mockups/Project Page.png -------------------------------------------------------------------------------- /mockups/Projects Page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/mockups/Projects Page.png -------------------------------------------------------------------------------- /mockups/Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/mockups/Settings.png -------------------------------------------------------------------------------- /nightwatch.conf.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | let nwBinary = 'nwjs/nw.exe'; 3 | let driver = 'nwjs/chromedriver.exe'; 4 | 5 | if (process.platform === 'linux') { 6 | nwBinary = 'nwjs/nw'; 7 | driver = 'nwjs/chromedriver'; 8 | } 9 | if (process.platform === 'darwin') { 10 | nwBinary = 'nwjs.app/contents/MacOS/nwjs'; 11 | driver = 'chromedriver'; 12 | } 13 | 14 | nwBinary = './node_modules/nw/' + nwBinary; 15 | driver = './node_modules/nw/' + driver; 16 | 17 | module.exports = (function (settings) { 18 | console.log(nwBinary); 19 | settings.webdriver.server_path = driver; 20 | settings.selenium.cli_args['webdriver.chrome.driver'] = driver; 21 | return settings; 22 | })(require('./nightwatch.json')); 23 | -------------------------------------------------------------------------------- /nightwatch.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nw-vue", 3 | "version": "1.6.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve --port=8964", 7 | "build": "npm run build:clean && npm run build:vue && npm run build:nw", 8 | "test:unit": "jest --config jest.config.js --coverage", 9 | "test:e2e": "vue-cli-service test:e2e", 10 | "lint": "vue-cli-service lint --no-fix", 11 | "build:clean": "rimraf ./dist-vue ./dist", 12 | "build:lin": "npm run build:lin:clean && npm run build:vue && build --tasks linux-x64 --mirror https://dl.nwjs.io/ .", 13 | "build:lin:clean": "rimraf ./dist-vue ./dist/nw-vue-1.6.0-linux-x64 ./dist/nw-vue-1.6.0-linux-x64.zip", 14 | "build:nw": "build --concurrent --tasks win-x86,linux-x86,linux-x64,mac-x64 --mirror https://dl.nwjs.io/ .", 15 | "build:vue": "vue-cli-service build --modern --dest ./dist-vue", 16 | "build:win": "npm run build:win:clean && npm run build:vue && build --tasks win-x86 --mirror https://dl.nwjs.io/ .", 17 | "build:win:clean": "rimraf ./dist-vue ./dist/nw-vue-1.6.0-win-x86 ./dist/nw-vue-1.6.0-win-x86.zip ./dist/nw-vue-1.6.0-win-x86.7z ./dist/nw-vue-1.6.0-win-x86-Setup.exe", 18 | "fix": "vue-cli-service lint --fix", 19 | "regression": "rd /s /q node_modules & rd /s /q node_modules & rd /s /q node_modules & npm install && npm run lint && npm test && npm run build:win && npm run run:win", 20 | "run:lin": "./dist/nw-vue-1.6.0-linux-x64/nw-vue", 21 | "run:win": "dist\\nw-vue-1.6.0-win-x86\\nw-vue.exe", 22 | "start": "concurrently \"npm run serve\" \"wait-on http://localhost:8964 && nw .\"", 23 | "test": "npm run test:unit", 24 | "update:vue-devtools": "rimraf ./node_modules/nw-vue-devtools-prebuilt && npm install", 25 | "validate": "npm run lint && npm run test:unit && npm run build:vue" 26 | }, 27 | "main": "http://localhost:8964", 28 | "dependencies": { 29 | "bootstrap": "4.5.0", 30 | "bootstrap-vue": "2.15.0", 31 | "express": "^4.17.1", 32 | "moment": "2.27.0", 33 | "pouchdb": "7.2.2", 34 | "pouchdb-find": "7.2.2", 35 | "relational-pouch": "4.0.0", 36 | "uuid": "8.3.0", 37 | "vue-router": "^3.2.0", 38 | "vue-select": "3.10.7", 39 | "vue2-datepicker": "3.6.1", 40 | "vuex": "^3.4.0" 41 | }, 42 | "devDependencies": { 43 | "@vue/cli-plugin-babel": "^4.4.6", 44 | "@vue/cli-plugin-e2e-nightwatch": "^4.4.6", 45 | "@vue/cli-plugin-eslint": "^4.4.6", 46 | "@vue/cli-plugin-router": "4.4.6", 47 | "@vue/cli-plugin-unit-jest": "^4.4.6", 48 | "@vue/cli-plugin-vuex": "4.4.6", 49 | "@vue/cli-service": "^4.4.6", 50 | "@vue/test-utils": "^1.0.3", 51 | "babel-eslint": "^10.1.0", 52 | "concurrently": "^5.2.0", 53 | "core-js": "^3.6.5", 54 | "eslint": "^7.4.0", 55 | "eslint-config-tjw-base": "^1.0.0", 56 | "eslint-config-tjw-vue": "^2.0.0", 57 | "eslint-plugin-jest": "^23.18.0", 58 | "eslint-plugin-vue": "^6.2.2", 59 | "eslint-plugin-vuejs-accessibility": "^0.3.1", 60 | "jest-serializer-vue-tjw": "^3.14.0", 61 | "nw": "0.47.0-sdk", 62 | "nw-vue-devtools-prebuilt": "^0.0.10", 63 | "nwjs-builder-phoenix": "^1.15.0", 64 | "nwjs-types": "^1.0.0", 65 | "rimraf": "^3.0.2", 66 | "vue": "^2.6.11", 67 | "vue-template-compiler": "^2.6.11", 68 | "wait-on": "^5.1.0" 69 | }, 70 | "ManifestComments": [ 71 | "Only add dependencies that you want shipped to the end user, for everything else, use devDependencies, including things that will be bundled by webpack.", 72 | "NW.js requires a name and a main, everything else is optional.", 73 | "The build section is used by nwjs-builder-phoenix, see its documentation for more info", 74 | "To turn spell checking off, remove it from the chromium-args in this file" 75 | ], 76 | "build": { 77 | "nwVersion": "v0.47.0", 78 | "nwFlavor": "normal", 79 | "targets": [ 80 | "zip", 81 | "nsis7z" 82 | ], 83 | "files": [ 84 | "**/*" 85 | ], 86 | "excludes": [ 87 | "e2e/**/*", 88 | "src/**/*", 89 | "tests/**/*", 90 | "public/**/*", 91 | "dist-vue/**/*.js.map", 92 | "dist-vue/**/*.css.map", 93 | "dist-vue/**/*-legacy*", 94 | ".browserslistrc", 95 | ".eslintrc", 96 | ".gitignore", 97 | ".editorconfig", 98 | "babel.config.js", 99 | "CODE_OF_CONDUCT.md", 100 | "cypress.json", 101 | "jest.config.js", 102 | "nightwatch.conf.js", 103 | "nightwatch.json", 104 | "package-lock.json", 105 | "screenshot.png", 106 | "selenium-debug.log", 107 | "postcss.config.js", 108 | "vue.config.js" 109 | ], 110 | "strippedProperties": [ 111 | "ManifestComments", 112 | "scripts", 113 | "devDependencies", 114 | "build" 115 | ], 116 | "overriddenProperties": { 117 | "main": "http://localhost:8965", 118 | "node-remote": "http://localhost:8965", 119 | "node-main": "server.js", 120 | "chromium-args": "--enable-spell-checking", 121 | "window": { 122 | "width": 960, 123 | "height": 600, 124 | "min_width": 700, 125 | "min_height": 500, 126 | "icon": "dist-vue/icon.png" 127 | } 128 | }, 129 | "win": { 130 | "icon": "public/icon-256.ico" 131 | }, 132 | "mac": { 133 | "icon": "public/icon.icns" 134 | }, 135 | "nsis": { 136 | "icon": "public/icon-256.ico", 137 | "unIcon": "public/icon.ico", 138 | "languages": [ 139 | "English" 140 | ], 141 | "diffUpdaters": false, 142 | "hashCalculation": true 143 | } 144 | }, 145 | "chromium-args": "--enable-spell-checking --load-extension='./node_modules/nw-vue-devtools-prebuilt/extension'", 146 | "node-main": "", 147 | "node-remote": "http://localhost:8964", 148 | "window": { 149 | "width": 960, 150 | "height": 600, 151 | "min_width": 700, 152 | "min_height": 500, 153 | "icon": "src/assets/vue.png" 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/public/icon-256.ico -------------------------------------------------------------------------------- /public/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/public/icon.icns -------------------------------------------------------------------------------- /public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/public/icon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/public/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | NwVue 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/screenshot.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | app.use(express.static('./dist-vue')); 4 | app.listen(8965); 5 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 55 | -------------------------------------------------------------------------------- /src/assets/nw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/src/assets/nw.png -------------------------------------------------------------------------------- /src/assets/vue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/src/assets/vue.png -------------------------------------------------------------------------------- /src/components/FsExample.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 59 | 60 | 86 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 177 | 178 | 179 | 196 | -------------------------------------------------------------------------------- /src/components/LinkList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 52 | 53 | 66 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' 3 | import vSelect from 'vue-select' 4 | import DatePicker from 'vue2-datepicker'; 5 | 6 | import 'bootstrap/dist/css/bootstrap.css' 7 | import 'bootstrap-vue/dist/bootstrap-vue.css' 8 | import 'vue-select/dist/vue-select.css'; 9 | import 'vue2-datepicker/index.css'; 10 | 11 | import App from './App.vue'; 12 | import router from './router'; 13 | import store from './store'; 14 | 15 | Vue.use(BootstrapVue) 16 | Vue.use(IconsPlugin) 17 | Vue.component('v-select', vSelect) 18 | Vue.component('date-picker', DatePicker) 19 | 20 | Vue.config.productionTip = false; 21 | 22 | // eslint-disable-next-line no-unused-vars 23 | const app = new Vue({ 24 | router, 25 | store, 26 | 27 | render: function (hyperscript) { 28 | return hyperscript(App); 29 | } 30 | }).$mount('#app'); 31 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import HoursLog from '../views/HoursLog.vue'; 4 | import TimeEntry from '../views/TimeEntry.vue'; 5 | import Projects from '../views/Projects.vue'; 6 | import ProjectEntry from '../views/ProjectEntry.vue'; 7 | import Settings from '../views/Settings.vue'; 8 | import SettingsEdit from '../views/SettingsEdit.vue'; 9 | 10 | Vue.use(VueRouter); 11 | 12 | const routes = [ 13 | { 14 | path: '/', 15 | name: 'HoursLog', 16 | component: HoursLog 17 | }, 18 | { 19 | path: '/add-time-entry', 20 | name: 'TimeEntry', 21 | component: TimeEntry 22 | }, 23 | { 24 | path: '/projects', 25 | name: 'Projects', 26 | component: Projects 27 | }, 28 | { 29 | path: '/add-project', 30 | name: 'ProjectEntry', 31 | component: ProjectEntry 32 | }, 33 | { 34 | path: '/settings', 35 | name: 'Settings', 36 | component: Settings 37 | }, 38 | { 39 | path: '/settings/edit', 40 | name: 'SettingsEdit', 41 | component: SettingsEdit 42 | } 43 | ]; 44 | 45 | const router = new VueRouter({ 46 | routes 47 | }); 48 | 49 | export default router; 50 | -------------------------------------------------------------------------------- /src/store/dbConfig.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwenf/vue-nwjs-hours-tracking/cf9bbd6d5dff0d89b605966ff389580b182dc53c/src/store/dbConfig.js -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Vue from 'vue'; 3 | import Vuex from 'vuex'; 4 | import PouchDB from 'pouchdb'; 5 | import RelPouch from 'relational-pouch'; 6 | import FindPouch from 'pouchdb-find'; 7 | import { v4 as uuidv4 } from 'uuid'; 8 | 9 | import router from '../router'; 10 | 11 | PouchDB.plugin(RelPouch); 12 | PouchDB.plugin(FindPouch); 13 | 14 | // let timeEntriesDB = new PouchDB('timeEntries'); 15 | // let projectsDB = new PouchDB('projects'); 16 | // PouchDB.debug.enable('*'); 17 | 18 | let db = new PouchDB('hourTracking'); 19 | db.setSchema([ 20 | { 21 | singular: 'timeEntry', 22 | plural: 'timeEntries', 23 | relations: { 24 | project: {belongsTo: 'project'} 25 | } 26 | }, 27 | { 28 | singular: 'project', 29 | plural: 'projects', 30 | relations: { 31 | timeEntries: {hasMany: 'timeEntry'} 32 | } 33 | }, 34 | { 35 | singular: 'user', 36 | plural: 'users' 37 | } 38 | ]); 39 | 40 | Vue.use(Vuex); 41 | 42 | export default new Vuex.Store({ 43 | state: { 44 | timeEntries: [], 45 | projects: [], 46 | user: null 47 | }, 48 | getters: { 49 | getProjects (state) { 50 | return state.projects.map((project) => { 51 | return { 52 | label: project.name, 53 | code: project.id 54 | }; 55 | }); 56 | } 57 | }, 58 | mutations: { 59 | setTimeEntries (state, payload) { 60 | state.timeEntries = payload 61 | }, 62 | setProjects (state, payload) { 63 | state.projects = payload 64 | }, 65 | setUser (state, payload) { 66 | state.user = payload 67 | } 68 | }, 69 | actions: { 70 | async readTimeEntries ({ commit }) { 71 | // const docs = await timeEntriesDB.allDocs({ 72 | // include_docs: true 73 | // }); 74 | const docs = await db.rel.find('timeEntry'); 75 | console.log(docs) 76 | commit('setTimeEntries', docs.timeEntries); 77 | }, 78 | async createTimeEntry ({}, payload) { 79 | try { 80 | // const res = await timeEntriesDB.put({ _id: uuidv4(), ...payload }); 81 | const res = await db.rel.save('timeEntry', { 82 | ...payload 83 | }); 84 | console.log('success', res); 85 | router.push({ name: 'HoursLog' }); 86 | } catch (err) { 87 | console.error(err); 88 | } 89 | }, 90 | async readProjects ({ commit }) { 91 | // const docs = await projectsDB.allDocs({ 92 | // include_docs: true 93 | // }); 94 | const docs = await db.rel.find('project'); 95 | console.log(docs.projects) 96 | commit('setProjects', docs.projects); 97 | }, 98 | async createProject ({}, payload) { 99 | try { 100 | // const res = await projectsDB.put({ _id: uuidv4(), ...payload }); 101 | const res = await db.rel.save('project', { 102 | ...payload 103 | }) 104 | router.push({ name: 'Projects' }); 105 | } catch (err) { 106 | console.error(err); 107 | } 108 | }, 109 | async createUser ({ commit }, payload) { 110 | try { 111 | // const res = await projectsDB.put({ _id: uuidv4(), ...payload }); 112 | const res = await db.rel.save('user', { 113 | ...payload 114 | }) 115 | console.log('user created', res) 116 | commit('setUser', { 117 | ...payload, 118 | id: res.id, 119 | }) 120 | router.push({ name: 'Settings' }); 121 | } catch (err) { 122 | console.error(err); 123 | } 124 | } 125 | }, 126 | modules: { 127 | } 128 | }); 129 | -------------------------------------------------------------------------------- /src/views/HoursLog.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 64 | -------------------------------------------------------------------------------- /src/views/ProjectEntry.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 73 | -------------------------------------------------------------------------------- /src/views/Projects.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 45 | -------------------------------------------------------------------------------- /src/views/Settings.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 49 | -------------------------------------------------------------------------------- /src/views/SettingsEdit.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 134 | 135 | -------------------------------------------------------------------------------- /src/views/TimeEntry.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 69 | -------------------------------------------------------------------------------- /tests/e2e/custom-assertions/elementCount.js: -------------------------------------------------------------------------------- 1 | // A custom Nightwatch assertion. 2 | // The assertion name is the filename. 3 | // Example usage: 4 | // 5 | // browser.assert.elementCount(selector, count) 6 | // 7 | // For more information on custom assertions see: 8 | // http://nightwatchjs.org/guide#writing-custom-assertions 9 | 10 | exports.assertion = function elementCount (selector, count) { 11 | this.message = 'Testing if element "' + selector + '" has count: ' + count; 12 | this.expected = count; 13 | this.pass = (value) => { 14 | return value === count; 15 | }; 16 | this.value = (result) => { 17 | return result.value; 18 | }; 19 | function evaluator (_selector) { 20 | return document.querySelectorAll(_selector).length; 21 | } 22 | this.command = (cb) => { 23 | return this.api.execute(evaluator, [selector], cb); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // For authoring Nightwatch tests, see 2 | // http://nightwatchjs.org/guide#usage 3 | 4 | module.exports = { 5 | 'default e2e tests': (browser) => { 6 | browser 7 | .url(process.env.VUE_DEV_SERVER_URL) 8 | .waitForElementVisible('#app', 5000) 9 | .assert.elementPresent('.hello') 10 | .assert.containsText('h1', 'Welcome to your Vue.js Desktop App in NW.js!') 11 | .assert.containsText('[data-test="versions]"', 'You are using Vue.js (v2.6.10), NW.js (v0.39.2-sdk), Node.js (v12.4.0), and Chromium (v75.0.3770.100).') 12 | .assert.elementCount('img', 2) 13 | .end(); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | }, 5 | rules: { 6 | 'import/no-extraneous-dependencies': 'off' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /tests/unit/App.test.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import App from '@/App.vue'; 3 | 4 | describe('App.vue', () => { 5 | test('Render default contents', () => { 6 | const wrapper = shallowMount(App); 7 | 8 | expect(wrapper) 9 | .toMatchSnapshot(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/__snapshots__/App.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App.vue Render default contents 1`] = ` 4 |
5 | 6 | 7 | 8 |
9 | `; 10 | -------------------------------------------------------------------------------- /tests/unit/components/FsExample.test.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import FsExample from '@/components/FsExample.vue'; 3 | 4 | describe('FsExample.vue', () => { 5 | test('Render default contents', () => { 6 | const wrapper = shallowMount(FsExample); 7 | 8 | expect(wrapper) 9 | .toMatchSnapshot(); 10 | }); 11 | 12 | test('Click button', async () => { 13 | const wrapper = shallowMount(FsExample); 14 | let domButton = wrapper.find('[data-test="fs-example-button"]'); 15 | domButton.trigger('click'); 16 | 17 | await wrapper.vm.$nextTick(); 18 | 19 | expect(window.nw.require) 20 | .toHaveBeenCalledWith('fs'); 21 | 22 | expect(wrapper) 23 | .toMatchSnapshot(); 24 | }); 25 | 26 | test('Error state', async () => { 27 | window.nw.require.mockImplementation((module) => { 28 | if (module === 'fs') { 29 | return new Error(); 30 | } 31 | }); 32 | 33 | const wrapper = shallowMount(FsExample); 34 | let domButton = wrapper.find('[data-test="fs-example-button"]'); 35 | domButton.trigger('click'); 36 | 37 | await wrapper.vm.$nextTick(); 38 | 39 | expect(window.nw.require) 40 | .toHaveBeenCalledWith('fs'); 41 | 42 | expect(wrapper) 43 | .toMatchSnapshot(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/unit/components/HelloWorld.test.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, mount } from '@vue/test-utils'; 2 | import HelloWorld from '@/components/HelloWorld.vue'; 3 | 4 | describe('HelloWorld.vue', () => { 5 | test('Render props.msg', () => { 6 | const msg = 'new message'; 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg } 9 | }); 10 | 11 | expect(wrapper.find('[data-test="message"]').text()) 12 | .toEqual(msg); 13 | }); 14 | 15 | test('Render default contents', () => { 16 | const wrapper = mount(HelloWorld); 17 | 18 | expect(wrapper) 19 | .toMatchSnapshot(); 20 | }); 21 | 22 | test('Activate dev tools', async () => { 23 | const wrapper = shallowMount(HelloWorld); 24 | 25 | const button = wrapper.find('[data-test="toggleDevTools"]'); 26 | 27 | button.trigger('click'); 28 | await wrapper.vm.$nextTick(); 29 | 30 | expect(wrapper.find('[data-test="toggleDevTools').html()) 31 | .toMatchSnapshot('hide'); 32 | 33 | button.trigger('click'); 34 | await wrapper.vm.$nextTick(); 35 | 36 | expect(wrapper.find('[data-test="toggleDevTools').html()) 37 | .toMatchSnapshot('show'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/unit/components/LinkList.test.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import LinkList from '@/components/LinkList.vue'; 3 | 4 | describe('LinkList.vue', () => { 5 | const link = { 6 | name: 'Site', 7 | url: 'https://nwjs.io' 8 | }; 9 | 10 | test('Validate props', () => { 11 | const wrapper = shallowMount(LinkList); 12 | const links = wrapper.vm.$options.props.links; 13 | 14 | expect(links.required) 15 | .toBeFalsy(); 16 | 17 | expect(links.type) 18 | .toBe(Array); 19 | 20 | expect(links.default) 21 | .toBeNull(); 22 | 23 | expect(links.validator && links.validator([{ name: '', url: '' }])) 24 | .toBeFalsy(); 25 | 26 | expect(links.validator && links.validator([link])) 27 | .toBeTruthy(); 28 | }); 29 | 30 | test('Render default contents', () => { 31 | const wrapper = shallowMount(LinkList, { 32 | propsData: { links: [link] } 33 | }); 34 | 35 | expect(wrapper) 36 | .toMatchSnapshot(); 37 | }); 38 | 39 | test('Click link', () => { 40 | const wrapper = shallowMount(LinkList, { 41 | propsData: { links: [link] } 42 | }); 43 | 44 | let domLink = wrapper.findAll('[data-test="link"]').at(0); 45 | domLink.trigger('click'); 46 | 47 | expect(window.nw.Shell.openExternal) 48 | .toHaveBeenCalledWith('https://nwjs.io'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/FsExample.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`FsExample.vue Click button 1`] = ` 4 |
5 | 6 | 9 |

10 | The contents of the current working directory: 11 |

12 |
13 |
14 | example-file-1.ext 15 |
16 |
17 | example-file-2.ext 18 |
19 |
20 |
21 | `; 22 | 23 | exports[`FsExample.vue Error state 1`] = ` 24 |
25 |
26 | There was an error attempting to read from the file system. 27 |
28 | 31 | 32 |
33 | `; 34 | 35 | exports[`FsExample.vue Render default contents 1`] = ` 36 |
37 | 38 | 41 | 42 |
43 | `; 44 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/HelloWorld.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`HelloWorld.vue Activate dev tools: hide 1`] = ` 4 | 9 | `; 10 | 11 | exports[`HelloWorld.vue Activate dev tools: show 1`] = ` 12 | 17 | `; 18 | 19 | exports[`HelloWorld.vue Render default contents 1`] = ` 20 |
21 |

22 | Welcome to your Vue Desktop App in NW.js! 23 |

24 |

25 | You are using 26 | Vue.js (v2.6.11), 27 | NW.js (v0.47.0-sdk), 28 | Node.js (v14.5.0), 29 | and 30 | Chromium (v84.0.4147.89). 31 |

32 | 37 |

You can use the resources below to find more information around building your Vue App.

38 | 65 |

Installed CLI Plugins

66 | 88 |

Essential Links

89 | 116 |

Ecosystem

117 | 144 |
145 |
146 | 147 | 150 | 151 |
152 |
153 | `; 154 | -------------------------------------------------------------------------------- /tests/unit/components/__snapshots__/LinkList.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`LinkList.vue Render default contents 1`] = ` 4 | 11 | `; 12 | -------------------------------------------------------------------------------- /tests/unit/setup.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | const { getComputedStyle } = window; 4 | 5 | Vue.config.productionTip = false; 6 | 7 | // Prevents console log message to install Vue DevTools 8 | Vue.config.devtools = false; 9 | 10 | // Monkeypatch JSDOM missing transition styles + vue-test-utils not properly stubbing transitions 11 | // in globally included libs (VeeValidate in our case) 12 | // https://github.com/vuejs/vue-test-utils/issues/839#issuecomment-410474714 13 | window.getComputedStyle = function getComputedStyleStub (el) { 14 | return { 15 | ...getComputedStyle(el), 16 | transitionDelay: '', 17 | transitionDuration: '', 18 | animationDelay: '', 19 | animationDuration: '' 20 | }; 21 | }; 22 | 23 | global.beforeEach(() => { 24 | window.process = { 25 | cwd: process.cwd, 26 | env: { 27 | NODE_ENV: 'development' 28 | }, 29 | versions: { 30 | chromium: '84.0.4147.89', 31 | nw: '0.47.0', 32 | 'nw-flavor': 'sdk', 33 | node: '14.5.0' 34 | } 35 | }; 36 | window.nw = { 37 | require: jest.fn((module) => { 38 | if (module === 'fs') { 39 | return { 40 | readdirSync: jest.fn(() => { 41 | return ['example-file-1.ext', 'example-file-2.ext']; 42 | }) 43 | }; 44 | } 45 | }), 46 | Shell: { 47 | openExternal: jest.fn() 48 | }, 49 | Window: { 50 | get: function () { 51 | return { 52 | showDevTools: jest.fn(), 53 | closeDevTools: jest.fn() 54 | }; 55 | } 56 | } 57 | }; 58 | }); 59 | 60 | global.afterEach(() => { 61 | window.nw.Window.get().showDevTools.mockClear(); 62 | }); 63 | 64 | 65 | // Jest's setTimeout defaults to 5 seconds. 66 | // Bump the timeout to 60 seconds. 67 | jest.setTimeout(60 * 1000); 68 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false 3 | }; 4 | --------------------------------------------------------------------------------