├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .node-version ├── .travis.yml ├── LICENSE ├── README.md ├── build ├── build.js ├── deploy.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── package.json ├── src ├── App.vue ├── assets │ ├── dayoff.png │ ├── overtime-two-hours.png │ ├── overtime.png │ └── regular.png ├── components │ ├── CurrentSolution.vue │ ├── Faq.vue │ ├── OffdayCondition.vue │ ├── OneRestOneOffSolution.vue │ ├── TwoOffSolution.vue │ ├── WeekTable.vue │ └── Workweek.vue ├── lib │ └── solutions.js └── main.js └── static ├── .gitkeep └── screenshot.png /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /.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 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | sourceType: 'module' 5 | }, 6 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 7 | extends: 'semistandard', 8 | // required to lint *.vue files 9 | plugins: [ 10 | 'html' 11 | ], 12 | // add your custom rules here 13 | 'rules': { 14 | // allow paren-less arrow functions 15 | 'arrow-parens': 0, 16 | // allow debugger during development 17 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | selenium-debug.log 6 | test/unit/coverage 7 | test/e2e/reports 8 | out 9 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v4.x 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | env: 5 | global: 6 | - GH_REF=github.com/g0v/workweek.git 7 | # GH_TOKEN 8 | - secure: "tgpgVQ/PRUqIqA4QrmUPi8Xk1i4iPbT5KxGlMLpHuRhdeKd+l6cm33/cfEJJbIa9ZxbEQsEoNGKpjZrD2dFmVASlKEZf408N8LMB09aBfaJNEIkxPS5Q+SRvJQNx01vrpKqxE09+OYCR8ORW0NlN8P7Lz3gWdkKJashb10Y4D1bChydCu9yZiCuxab3jelff+AXYvtuSYhWdrma/9GVy0dP56L6Iaz8yPABmkPnoMTCljd3VcWmGqe1r3f9fOOjIun63lTK6TZUaWfE59Ahbhp+XDAzaa67axF2o031HyTpzn/wpkzT12YrYiNVHoTcq4N1clV3BjogjJHRGqrZBMVo04M36z3Xg3UR5cP+nP5eaq5hLWhL01XvQlYeScuAECs0T6HyGpBjmNT3rGXIdlEYc08/ywlKxYFCufGYMnlL7vbJepnt5c7WnBbKmRFW7HZem2+PIKdmA9VN0/6sqj9A774BLiNVUAGfzjKl0Nhq4otf5oKqyr7gAFZNEe4nKMVVpeFk+rbOVFmRkFi9kmT2J9DBWmxb6jvT1cH5cY6dQc63aPbBL/2FHjOI/q/tHmTAJYgflLm1Jdx9stidY4o/NlYI2EnM/jwmlVGOj0IOQvcY7cGMEwhPKKBndBjxjzSweyFo3ROvWf1A4iHV5wQKIYgsMSnJEUMO9117Avh0=" 9 | 10 | cache: node_modules 11 | 12 | script: 13 | - npm run build 14 | 15 | after_success: npm run deploy 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Yuren Ju 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # workweek 2 | 3 | > A Vue.js project 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # run unit tests 18 | npm run unit 19 | 20 | # run e2e tests 21 | npm run e2e 22 | 23 | # run all tests 24 | npm test 25 | ``` 26 | 27 | For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 28 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('shelljs/global') 3 | env.NODE_ENV = 'production' 4 | 5 | var path = require('path') 6 | var config = require('../config') 7 | var ora = require('ora') 8 | var webpack = require('webpack') 9 | var webpackConfig = require('./webpack.prod.conf') 10 | 11 | console.log( 12 | ' Tip:\n' + 13 | ' Built files are meant to be served over an HTTP server.\n' + 14 | ' Opening index.html over file:// won\'t work.\n' 15 | ) 16 | 17 | var spinner = ora('building for production...') 18 | spinner.start() 19 | 20 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) 21 | rm('-rf', assetsPath) 22 | mkdir('-p', assetsPath) 23 | cp('-R', 'static/', assetsPath) 24 | 25 | webpack(webpackConfig, function (err, stats) { 26 | spinner.stop() 27 | if (err) throw err 28 | process.stdout.write(stats.toString({ 29 | colors: true, 30 | modules: false, 31 | children: false, 32 | chunks: false, 33 | chunkModules: false 34 | }) + '\n') 35 | }) 36 | -------------------------------------------------------------------------------- /build/deploy.js: -------------------------------------------------------------------------------- 1 | require('shelljs/global'); 2 | 3 | rm('-rf', 'out'); 4 | exec('git clone "https://' + env.GH_TOKEN + 5 | '@' + env.GH_REF + '" --depth 1 -b gh-pages out'); 6 | pushd('out'); 7 | exec('git config user.name "Automatic Commit"'); 8 | exec('git config user.email "workweek@g0v.tw"'); 9 | exec('git rm -rf .'); 10 | cp('-r', '../dist/', '.'); 11 | exec('git add .'); 12 | exec('git commit -m "Automatic commit: ' + Date() + '"'); 13 | exec('git push "https://' + env.GH_TOKEN + 14 | '@' + env.GH_REF + '" gh-pages', {silent: true}); 15 | popd(); 16 | exit(0); 17 | -------------------------------------------------------------------------------- /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 | var path = require('path') 2 | var express = require('express') 3 | var webpack = require('webpack') 4 | var config = require('../config') 5 | var proxyMiddleware = require('http-proxy-middleware') 6 | var webpackConfig = process.env.NODE_ENV === 'testing' 7 | ? require('./webpack.prod.conf') 8 | : require('./webpack.dev.conf') 9 | 10 | // default port where dev server listens for incoming traffic 11 | var port = process.env.PORT || config.dev.port 12 | // Define HTTP proxies to your custom API backend 13 | // https://github.com/chimurai/http-proxy-middleware 14 | var proxyTable = config.dev.proxyTable 15 | 16 | var app = express() 17 | var compiler = webpack(webpackConfig) 18 | 19 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 20 | publicPath: webpackConfig.output.publicPath, 21 | stats: { 22 | colors: true, 23 | chunks: false 24 | } 25 | }) 26 | 27 | var hotMiddleware = require('webpack-hot-middleware')(compiler) 28 | // force page reload when html-webpack-plugin template changes 29 | compiler.plugin('compilation', function (compilation) { 30 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 31 | hotMiddleware.publish({ action: 'reload' }) 32 | cb() 33 | }) 34 | }) 35 | 36 | // proxy api requests 37 | Object.keys(proxyTable).forEach(function (context) { 38 | var options = proxyTable[context] 39 | if (typeof options === 'string') { 40 | options = { target: options } 41 | } 42 | app.use(proxyMiddleware(context, options)) 43 | }) 44 | 45 | // handle fallback for HTML5 history API 46 | app.use(require('connect-history-api-fallback')()) 47 | 48 | // serve webpack bundle output 49 | app.use(devMiddleware) 50 | 51 | // enable hot-reload and state-preserving 52 | // compilation error display 53 | app.use(hotMiddleware) 54 | 55 | // serve pure static assets 56 | var staticPath = path.posix.join(config.build.assetsPublicPath, config.build.assetsSubDirectory) 57 | app.use(staticPath, express.static('./static')) 58 | 59 | module.exports = app.listen(port, function (err) { 60 | if (err) { 61 | console.log(err) 62 | return 63 | } 64 | console.log('Listening at http://localhost:' + port + '\n') 65 | }) 66 | -------------------------------------------------------------------------------- /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 | return path.posix.join(config.build.assetsSubDirectory, _path) 7 | } 8 | 9 | exports.cssLoaders = function (options) { 10 | options = options || {} 11 | // generate loader string to be used with extract text plugin 12 | function generateLoaders (loaders) { 13 | var sourceLoader = loaders.map(function (loader) { 14 | var extraParamChar 15 | if (/\?/.test(loader)) { 16 | loader = loader.replace(/\?/, '-loader?') 17 | extraParamChar = '&' 18 | } else { 19 | loader = loader + '-loader' 20 | extraParamChar = '?' 21 | } 22 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 23 | }).join('!') 24 | 25 | if (options.extract) { 26 | return ExtractTextPlugin.extract('vue-style-loader', sourceLoader) 27 | } else { 28 | return ['vue-style-loader', sourceLoader].join('!') 29 | } 30 | } 31 | 32 | // http://vuejs.github.io/vue-loader/configurations/extract-css.html 33 | return { 34 | css: generateLoaders(['css']), 35 | postcss: generateLoaders(['css']), 36 | less: generateLoaders(['css', 'less']), 37 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 38 | scss: generateLoaders(['css', 'sass']), 39 | stylus: generateLoaders(['css', 'stylus']), 40 | styl: generateLoaders(['css', 'stylus']) 41 | } 42 | } 43 | 44 | // Generate loaders for standalone style files (outside of .vue) 45 | exports.styleLoaders = function (options) { 46 | var output = [] 47 | var loaders = exports.cssLoaders(options) 48 | for (var extension in loaders) { 49 | var loader = loaders[extension] 50 | output.push({ 51 | test: new RegExp('\\.' + extension + '$'), 52 | loader: loader 53 | }) 54 | } 55 | return output 56 | } 57 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var projectRoot = path.resolve(__dirname, '../') 5 | 6 | module.exports = { 7 | entry: { 8 | app: './src/main.js' 9 | }, 10 | output: { 11 | path: config.build.assetsRoot, 12 | publicPath: config.build.assetsPublicPath, 13 | filename: '[name].js' 14 | }, 15 | resolve: { 16 | extensions: ['', '.js', '.vue'], 17 | fallback: [path.join(__dirname, '../node_modules')], 18 | alias: { 19 | 'src': path.resolve(__dirname, '../src'), 20 | 'assets': path.resolve(__dirname, '../src/assets'), 21 | 'components': path.resolve(__dirname, '../src/components') 22 | } 23 | }, 24 | resolveLoader: { 25 | fallback: [path.join(__dirname, '../node_modules')] 26 | }, 27 | module: { 28 | preLoaders: [ 29 | { 30 | test: /\.vue$/, 31 | loader: 'eslint', 32 | include: projectRoot, 33 | exclude: /node_modules/ 34 | }, 35 | { 36 | test: /\.js$/, 37 | loader: 'eslint', 38 | include: projectRoot, 39 | exclude: /node_modules/ 40 | } 41 | ], 42 | loaders: [ 43 | { 44 | test: /\.vue$/, 45 | loader: 'vue' 46 | }, 47 | { 48 | test: /\.js$/, 49 | loader: 'babel', 50 | include: projectRoot, 51 | exclude: /node_modules/ 52 | }, 53 | { 54 | test: /\.json$/, 55 | loader: 'json' 56 | }, 57 | { 58 | test: /\.html$/, 59 | loader: 'vue-html' 60 | }, 61 | { 62 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 63 | loader: 'url', 64 | query: { 65 | limit: 10000, 66 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 67 | } 68 | }, 69 | { 70 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 71 | loader: 'url', 72 | query: { 73 | limit: 10000, 74 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 75 | } 76 | } 77 | ] 78 | }, 79 | eslint: { 80 | formatter: require('eslint-friendly-formatter') 81 | }, 82 | vue: { 83 | loaders: utils.cssLoaders() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var config = require('../config') 2 | var webpack = require('webpack') 3 | var merge = require('webpack-merge') 4 | var utils = require('./utils') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | 8 | // add hot-reload related code to entry chunks 9 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 10 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 11 | }) 12 | 13 | module.exports = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders() 16 | }, 17 | // eval-source-map is faster for development 18 | devtool: '#eval-source-map', 19 | plugins: [ 20 | new webpack.DefinePlugin({ 21 | 'process.env': config.dev.env 22 | }), 23 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 24 | new webpack.optimize.OccurenceOrderPlugin(), 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }) 33 | ] 34 | }) 35 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var env = process.env.NODE_ENV === 'testing' 10 | ? require('../config/test.env') 11 | : config.build.env 12 | 13 | var webpackConfig = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }) 16 | }, 17 | devtool: config.build.productionSourceMap ? '#source-map' : false, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 21 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 22 | }, 23 | vue: { 24 | loaders: utils.cssLoaders({ 25 | sourceMap: config.build.productionSourceMap, 26 | extract: true 27 | }) 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | } 38 | }), 39 | new webpack.optimize.OccurenceOrderPlugin(), 40 | // extract css into its own file 41 | new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), 42 | // generate dist index.html with correct asset hash for caching. 43 | // you can customize output by editing /index.html 44 | // see https://github.com/ampedandwired/html-webpack-plugin 45 | new HtmlWebpackPlugin({ 46 | filename: process.env.NODE_ENV === 'testing' 47 | ? 'index.html' 48 | : config.build.index, 49 | template: 'index.html', 50 | inject: true, 51 | minify: { 52 | removeComments: true, 53 | collapseWhitespace: true, 54 | removeAttributeQuotes: true 55 | // more options: 56 | // https://github.com/kangax/html-minifier#options-quick-reference 57 | }, 58 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 59 | chunksSortMode: 'dependency' 60 | }), 61 | // split vendor js into its own file 62 | new webpack.optimize.CommonsChunkPlugin({ 63 | name: 'vendor', 64 | minChunks: function (module, count) { 65 | // any required modules inside node_modules are extracted to vendor 66 | return ( 67 | module.resource && 68 | /\.js$/.test(module.resource) && 69 | module.resource.indexOf( 70 | path.join(__dirname, '../node_modules') 71 | ) === 0 72 | ) 73 | } 74 | }), 75 | // extract webpack runtime and module manifest to its own file in order to 76 | // prevent vendor hash from being updated whenever app bundle is updated 77 | new webpack.optimize.CommonsChunkPlugin({ 78 | name: 'manifest', 79 | chunks: ['vendor'] 80 | }) 81 | ] 82 | }) 83 | 84 | if (config.build.productionGzip) { 85 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 86 | 87 | webpackConfig.plugins.push( 88 | new CompressionWebpackPlugin({ 89 | asset: '[path].gz[query]', 90 | algorithm: 'gzip', 91 | test: new RegExp( 92 | '\\.(' + 93 | config.build.productionGzipExtensions.join('|') + 94 | ')$' 95 | ), 96 | threshold: 10240, 97 | minRatio: 0.8 98 | }) 99 | ) 100 | } 101 | 102 | module.exports = webpackConfig 103 | -------------------------------------------------------------------------------- /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: process.env.NODE_ENV === 'production' ? './' :'/', 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 | }, 19 | dev: { 20 | env: require('./dev.env'), 21 | port: 8080, 22 | proxyTable: {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | g0v 勞基法修法計算機 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workweek", 3 | "version": "1.0.0", 4 | "description": "A Vue.js project", 5 | "author": "Yuren Ju ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "build": "node build/build.js", 10 | "test": "echo \"No tests\"", 11 | "lint": "eslint --ext .js,.vue src", 12 | "deploy": "node build/deploy.js" 13 | }, 14 | "dependencies": { 15 | "babel-runtime": "^6.0.0", 16 | "query-string": "^4.2.2", 17 | "vue": "^1.0.21", 18 | "vue-router": "^0.7.13" 19 | }, 20 | "devDependencies": { 21 | "babel-core": "^6.0.0", 22 | "babel-loader": "^6.0.0", 23 | "babel-plugin-transform-runtime": "^6.0.0", 24 | "babel-preset-es2015": "^6.0.0", 25 | "babel-preset-stage-2": "^6.0.0", 26 | "connect-history-api-fallback": "^1.1.0", 27 | "css-loader": "^0.23.0", 28 | "eslint": "^2.10.2", 29 | "eslint-friendly-formatter": "^2.0.5", 30 | "eslint-loader": "^1.3.0", 31 | "eslint-plugin-html": "^1.3.0", 32 | "eslint-config-standard": "^5.1.0", 33 | "eslint-config-semistandard": "^6.0.2", 34 | "eslint-plugin-promise": "^1.0.8", 35 | "eslint-plugin-standard": "^1.3.2", 36 | "eventsource-polyfill": "^0.9.6", 37 | "express": "^4.13.3", 38 | "extract-text-webpack-plugin": "^1.0.1", 39 | "file-loader": "^0.8.4", 40 | "function-bind": "^1.0.2", 41 | "html-webpack-plugin": "^2.8.1", 42 | "http-proxy-middleware": "^0.12.0", 43 | "json-loader": "^0.5.4", 44 | "lolex": "^1.4.0", 45 | "mocha": "^2.4.5", 46 | "chai": "^3.5.0", 47 | "sinon": "^1.17.3", 48 | "sinon-chai": "^2.8.0", 49 | "inject-loader": "^2.0.1", 50 | "isparta-loader": "^2.0.0", 51 | "cross-spawn": "^2.1.5", 52 | "ora": "^0.2.0", 53 | "shelljs": "^0.6.0", 54 | "url-loader": "^0.5.7", 55 | "vue-hot-reload-api": "^1.2.0", 56 | "vue-html-loader": "^1.0.0", 57 | "vue-loader": "^8.3.0", 58 | "vue-style-loader": "^1.0.0", 59 | "webpack": "^1.12.2", 60 | "webpack-dev-middleware": "^1.4.0", 61 | "webpack-hot-middleware": "^2.6.0", 62 | "webpack-merge": "^0.8.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 44 | 45 | 56 | -------------------------------------------------------------------------------- /src/assets/dayoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0v/workweek/341a45c740e9bfad7095bb04ca83771196dd61e5/src/assets/dayoff.png -------------------------------------------------------------------------------- /src/assets/overtime-two-hours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0v/workweek/341a45c740e9bfad7095bb04ca83771196dd61e5/src/assets/overtime-two-hours.png -------------------------------------------------------------------------------- /src/assets/overtime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0v/workweek/341a45c740e9bfad7095bb04ca83771196dd61e5/src/assets/overtime.png -------------------------------------------------------------------------------- /src/assets/regular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0v/workweek/341a45c740e9bfad7095bb04ca83771196dd61e5/src/assets/regular.png -------------------------------------------------------------------------------- /src/components/CurrentSolution.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | -------------------------------------------------------------------------------- /src/components/Faq.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | 40 | 49 | -------------------------------------------------------------------------------- /src/components/OffdayCondition.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 35 | 36 | -------------------------------------------------------------------------------- /src/components/OneRestOneOffSolution.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /src/components/TwoOffSolution.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 35 | -------------------------------------------------------------------------------- /src/components/WeekTable.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/Workweek.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 176 | 177 | 178 | 236 | -------------------------------------------------------------------------------- /src/lib/solutions.js: -------------------------------------------------------------------------------- 1 | const REGULAR_HOURS_PER_DAY = 8; 2 | const TOTAL_WEEK_PER_YEAR = 52; 3 | const DAY_NAMES = ['一', '二', '三', '四', '五', '六', '日']; 4 | const STATE = { 5 | OFF: 0, 6 | REGULAR_WORK: 1, 7 | OVERTIME_WORK: 2, 8 | OVER_TWO_HOURS_WORK: 3, 9 | DAYOFF_WORK: 4, 10 | DAYOFF_ILLEGAL_WORK: 5 11 | }; 12 | 13 | function normalize (workhours) { 14 | let hours = workhours.slice().map(h => { 15 | h = parseFloat(h); 16 | if (isNaN(h)) { 17 | h = 0; 18 | } 19 | h = Math.min(24, h); 20 | h = Math.max(0, h); 21 | return h; 22 | }); 23 | return hours; 24 | } 25 | 26 | function current (workhours, hourlyPay, reason) { 27 | let workingMatrix = []; 28 | let total = 0; 29 | let overtimeHours = 0; 30 | let overtimeHoursTotal = 0; 31 | let pay = 0; 32 | let holiday = 19; 33 | let extraDayoff = 0; 34 | let illegal = false; 35 | let illegalReason; 36 | 37 | workhours = normalize(workhours); 38 | if (reason === 'laborDisagree') { 39 | workhours[6] = 0; 40 | } 41 | 42 | if (workhours[6] > 0 && reason === 'disaster') { 43 | extraDayoff = 1; 44 | } 45 | 46 | if (workhours[6] > 0 && reason === 'laborAgree') { 47 | illegal = true; 48 | illegalReason = '違法:非天災、' + 50 | '事變或突發事件禁止於 例假日(週日) 工作, ' + 53 | '違者處 2 萬以上 30 ' + 55 | '萬以下罰鍰 。'; 56 | } 57 | 58 | workhours.forEach((workhour, dayOfWeek) => { 59 | let workday = Array.apply(null, Array(12)).map((val, i) => { 60 | let currentState = STATE.OFF; 61 | 62 | if (workhour > i) { 63 | total++; 64 | } 65 | 66 | if (workhour <= i) { 67 | currentState = STATE.OFF; 68 | } else if (dayOfWeek === 6) { 69 | currentState = reason === 'disaster' ? STATE.DAYOFF_WORK : STATE.DAYOFF_ILLEGAL_WORK; 70 | } else if (total - overtimeHours > 40 && i - REGULAR_HOURS_PER_DAY >= 2) { 71 | currentState = STATE.OVER_TWO_HOURS_WORK; 72 | overtimeHours++; 73 | } else if (total - overtimeHours > 40 && i - REGULAR_HOURS_PER_DAY < 2) { 74 | currentState = STATE.OVERTIME_WORK; 75 | overtimeHours++; 76 | } else if (total - overtimeHours > 42) { 77 | currentState = STATE.OVER_TWO_HOURS_WORK; 78 | overtimeHours++; 79 | } else if (total - overtimeHours > 40) { 80 | currentState = STATE.OVERTIME_WORK; 81 | overtimeHours++; 82 | } else if (i < REGULAR_HOURS_PER_DAY) { 83 | currentState = STATE.REGULAR_WORK; 84 | } else if (i - REGULAR_HOURS_PER_DAY >= 2) { 85 | currentState = STATE.OVER_TWO_HOURS_WORK; 86 | overtimeHours++; 87 | } else if (i - REGULAR_HOURS_PER_DAY < 2) { 88 | currentState = STATE.OVERTIME_WORK; 89 | overtimeHours++; 90 | } else { 91 | currentState = STATE.OFF; 92 | } 93 | 94 | return currentState; 95 | }); 96 | workingMatrix.push(workday); 97 | 98 | if (dayOfWeek < 5) { 99 | let overtimeHours = workhour - 8; 100 | if (overtimeHours > 0 && overtimeHours <= 2) { 101 | pay += hourlyPay * 4 / 3 * overtimeHours; 102 | } else if (overtimeHours > 2) { 103 | pay += hourlyPay * 4 / 3 * 2 + hourlyPay * 5 / 3 * (overtimeHours - 2); 104 | } 105 | } 106 | 107 | if (dayOfWeek < 5 || (dayOfWeek === 6 && reason !== 'disaster')) { 108 | overtimeHoursTotal += workhour - 8 > 0 ? workhour - 8 : 0; 109 | } else if (dayOfWeek === 5) { 110 | overtimeHoursTotal += workhour; 111 | } 112 | }); 113 | var transposed = workingMatrix[0].map(function (col, i) { 114 | return workingMatrix.map(function (row) { 115 | return row[i]; 116 | }); 117 | }); 118 | 119 | // 週六工作的薪資規則 120 | // 前兩個小時,時薪為 1/3 121 | // 2 ~ 8 小時,時薪為 2/3 122 | // 超過第八個小時,時薪為 1 + 2/3 123 | // 如果是 7 7 7 7 7 5 0 的狀況,則週六不該為加班日 124 | if (workhours[5] > 0) { 125 | let sum = workhours.slice(0, 5).reduce((a, b) => parseFloat(a) + parseFloat(b)); 126 | let hours = workhours[5]; 127 | if (sum <= 40) { 128 | let diff = 40 - sum; 129 | hours = hours - diff; 130 | } 131 | 132 | if (hours < 0) { 133 | // do nothing 134 | } else if (hours <= 2) { 135 | pay += hours * hourlyPay * 1 / 3; 136 | } else if (hours <= 8) { 137 | pay += 2 * hourlyPay * 1 / 3 + (hours - 2) * hourlyPay * 2 / 3; 138 | } else if (hours > 8) { 139 | pay += 2 * hourlyPay * 1 / 3 + 6 * hourlyPay * 2 / 3 + 140 | (hours - 8) * hourlyPay * 5 / 3; 141 | } 142 | } 143 | 144 | // 週日工作的薪資規則,為什麼搞得這麼複雜? 145 | // 例假日工作八個小時以內,薪水皆以 hourlyPay * 8 計算 146 | // 超過八個小時合法的狀況薪水應為兩倍 147 | if (workhours[6] > 0 && workhours[6] <= 8) { 148 | pay += hourlyPay * 8; 149 | } else if (workhours[6] > 8) { 150 | if (reason === 'disaster') { 151 | pay += (workhours[6] - 8) * hourlyPay * 2 + hourlyPay * 8; 152 | } else if (workhours[6] <= 10) { 153 | pay += (workhours[6] - 8) * hourlyPay * 4 / 3 + hourlyPay * 8; 154 | } else if (workhours[6] > 10) { 155 | pay += (workhours[6] - 10) * hourlyPay * 5 / 3 + 2 * hourlyPay * 4 / 3 + hourlyPay * 8; 156 | } 157 | } 158 | 159 | let dayoff = 7; 160 | workhours.slice(0, 6).forEach(d => { 161 | if (d > 0) { 162 | dayoff--; 163 | } 164 | }); 165 | 166 | return { 167 | holiday: holiday, 168 | workingMatrix: workingMatrix, 169 | transposed: transposed, 170 | overtimePay: parseFloat(pay), 171 | overtimeHoursTotal: overtimeHoursTotal, 172 | totalWorkdays: 365 - holiday - dayoff * TOTAL_WEEK_PER_YEAR, 173 | extraDayoff: extraDayoff, 174 | illegal: illegal, 175 | illegalReason: illegalReason 176 | }; 177 | } 178 | 179 | function oneRestOneOff (workhours, hourlyPay, reason) { 180 | let workingMatrix = []; 181 | let total = 0; 182 | let overtimeHours = 0; 183 | let overtimeHoursTotal = 0; 184 | let pay = 0; 185 | let holiday = 12; 186 | let extraDayoff = 0; 187 | let illegal = false; 188 | let illegalReason; 189 | 190 | workhours = normalize(workhours); 191 | if (reason === 'laborDisagree') { 192 | workhours[6] = 0; 193 | } 194 | 195 | if (workhours[6] > 0 && reason === 'disaster') { 196 | extraDayoff = 1; 197 | } 198 | 199 | if (workhours[6] > 0 && reason === 'laborAgree') { 200 | illegal = true; 201 | illegalReason = '違法:非天災、' + 203 | '事變或突發事件禁止於 例假日(週日) 工作, ' + 206 | '違者處 2 萬以上 30 ' + 208 | '萬以下罰鍰 。'; 209 | } 210 | 211 | workhours.forEach((workhour, dayOfWeek) => { 212 | let workday = Array.apply(null, Array(12)).map((val, i) => { 213 | let currentState = STATE.OFF; 214 | 215 | if (workhour > i) { 216 | total++; 217 | } 218 | 219 | if (workhour <= i) { 220 | currentState = STATE.OFF; 221 | } else if (dayOfWeek === 6) { 222 | currentState = reason === 'disaster' ? STATE.DAYOFF_WORK : STATE.DAYOFF_ILLEGAL_WORK; 223 | } else if (total - overtimeHours > 40 && i - REGULAR_HOURS_PER_DAY >= 2) { 224 | currentState = STATE.OVER_TWO_HOURS_WORK; 225 | overtimeHours++; 226 | } else if (total - overtimeHours > 40 && i - REGULAR_HOURS_PER_DAY < 2) { 227 | currentState = STATE.OVERTIME_WORK; 228 | overtimeHours++; 229 | } else if (total - overtimeHours > 42) { 230 | currentState = STATE.OVER_TWO_HOURS_WORK; 231 | overtimeHours++; 232 | } else if (total - overtimeHours > 40) { 233 | currentState = STATE.OVERTIME_WORK; 234 | overtimeHours++; 235 | } else if (i < REGULAR_HOURS_PER_DAY) { 236 | currentState = STATE.REGULAR_WORK; 237 | } else if (i - REGULAR_HOURS_PER_DAY >= 2) { 238 | currentState = STATE.OVER_TWO_HOURS_WORK; 239 | overtimeHours++; 240 | } else if (i - REGULAR_HOURS_PER_DAY < 2) { 241 | currentState = STATE.OVERTIME_WORK; 242 | overtimeHours++; 243 | } else { 244 | currentState = STATE.OFF; 245 | } 246 | 247 | return currentState; 248 | }); 249 | workingMatrix.push(workday); 250 | 251 | if (dayOfWeek < 5) { 252 | let overtimeHours = workhour - 8; 253 | if (overtimeHours > 0 && overtimeHours <= 2) { 254 | pay += hourlyPay * 4 / 3 * overtimeHours; 255 | } else if (overtimeHours > 2) { 256 | pay += hourlyPay * 4 / 3 * 2 + hourlyPay * 5 / 3 * (overtimeHours - 2); 257 | } 258 | } 259 | 260 | if (dayOfWeek < 5 || (dayOfWeek === 6 && reason !== 'disaster')) { 261 | overtimeHoursTotal += workhour - 8 > 0 ? workhour - 8 : 0; 262 | } else if (dayOfWeek === 5) { 263 | if (workhour > 0 && workhour <= 4) { 264 | overtimeHoursTotal += 4; 265 | } else if (workhour > 0 && workhour <= 8) { 266 | overtimeHoursTotal += 8; 267 | } else if (workhour > 0 && workhour <= 12) { 268 | overtimeHoursTotal += 12; 269 | } else { 270 | overtimeHoursTotal += workhour; 271 | } 272 | } 273 | }); 274 | var transposed = workingMatrix[0].map(function (col, i) { 275 | return workingMatrix.map(function (row) { 276 | return row[i]; 277 | }); 278 | }); 279 | 280 | // http://www.cna.com.tw/news/firstnews/201606290106-1.aspx 281 | // 根據勞動部資料,休息日工資計算的部分,擬從原本的加倍發給,改為在2小時以內者,按平日工資 282 | // 額另給予每小時1又1/3,再繼續工作者另給予每小時1又2/3。工作時間計算方式為工作4小時以內 283 | // ,以4小時計算,超過4小時至8小時,以8小時計算,超過8小時至12小時以內者,以12小時計。 284 | 285 | if (workhours[5] > 0) { 286 | if (workhours[5] <= 4) { 287 | pay += hourlyPay * 2 * 4 / 3 + hourlyPay * 2 * 5 / 3; 288 | } else if (workhours[5] <= 8) { 289 | pay += hourlyPay * 2 * 4 / 3 + hourlyPay * 6 * 5 / 3; 290 | } else { 291 | pay += hourlyPay * 2 * 4 / 3 + hourlyPay * 10 * 5 / 3; 292 | } 293 | } 294 | 295 | // 週日工作的薪資規則,為什麼搞得這麼複雜? 296 | // 例假日工作八個小時以內,薪水皆以 hourlyPay * 8 計算 297 | // 超過八個小時合法的狀況薪水應為兩倍 298 | if (workhours[6] > 0 && workhours[6] <= 8) { 299 | pay += hourlyPay * 8; 300 | } else if (workhours[6] > 8) { 301 | if (reason === 'disaster') { 302 | pay += (workhours[6] - 8) * hourlyPay * 2 + hourlyPay * 8; 303 | } else if (workhours[6] <= 10) { 304 | pay += (workhours[6] - 8) * hourlyPay * 4 / 3 + hourlyPay * 8; 305 | } else if (workhours[6] > 10) { 306 | pay += (workhours[6] - 10) * hourlyPay * 5 / 3 + 2 * hourlyPay * 4 / 3 + hourlyPay * 8; 307 | } 308 | } 309 | 310 | let dayoff = 7; 311 | workhours.slice(0, 6).forEach(d => { 312 | if (d > 0) { 313 | dayoff--; 314 | } 315 | }); 316 | 317 | return { 318 | holiday: holiday, 319 | workingMatrix: workingMatrix, 320 | transposed: transposed, 321 | overtimePay: parseFloat(pay), 322 | overtimeHoursTotal: overtimeHoursTotal, 323 | totalWorkdays: 365 - holiday - dayoff * TOTAL_WEEK_PER_YEAR, 324 | extraDayoff: extraDayoff, 325 | illegal: illegal, 326 | illegalReason: illegalReason 327 | }; 328 | } 329 | 330 | function twoOff (workhours, hourlyPay, reason) { 331 | let workingMatrix = []; 332 | let total = 0; 333 | let overtimeHours = 0; 334 | let overtimeHoursTotal = 0; 335 | let pay = 0; 336 | let holiday = 19; 337 | let extraDayoff = 0; 338 | let illegal = false; 339 | let illegalReason; 340 | 341 | workhours = normalize(workhours); 342 | if (reason === 'laborDisagree') { 343 | workhours[5] = 0; 344 | workhours[6] = 0; 345 | } 346 | 347 | if ((workhours[6] > 0 || workhours[5] > 0) && reason === 'disaster') { 348 | extraDayoff = (workhours[6] > 0 ? 1 : 0) + (workhours[5] > 0 ? 1 : 0); 349 | } 350 | 351 | if ((workhours[6] > 0 || workhours[5] > 0) && reason === 'laborAgree') { 352 | illegal = true; 353 | illegalReason = '違法:非天災、' + 355 | '事變或突發事件禁止於 例假日(週日與週六) 工作, ' + 358 | '違者處 2 萬以上 30 ' + 360 | '萬以下罰鍰 。'; 361 | } 362 | 363 | workhours.forEach((workhour, dayOfWeek) => { 364 | let workday = Array.apply(null, Array(12)).map((val, i) => { 365 | let currentState = STATE.OFF; 366 | 367 | if (workhour > i) { 368 | total++; 369 | } 370 | 371 | if (workhour <= i) { 372 | currentState = STATE.OFF; 373 | } else if (dayOfWeek === 5 || dayOfWeek === 6) { 374 | currentState = reason === 'disaster' ? STATE.DAYOFF_WORK : STATE.DAYOFF_ILLEGAL_WORK; 375 | } else if (total - overtimeHours > 40 && i - REGULAR_HOURS_PER_DAY >= 2) { 376 | currentState = STATE.OVER_TWO_HOURS_WORK; 377 | overtimeHours++; 378 | } else if (total - overtimeHours > 40 && i - REGULAR_HOURS_PER_DAY < 2) { 379 | currentState = STATE.OVERTIME_WORK; 380 | overtimeHours++; 381 | } else if (total - overtimeHours > 42) { 382 | currentState = STATE.OVER_TWO_HOURS_WORK; 383 | overtimeHours++; 384 | } else if (total - overtimeHours > 40) { 385 | currentState = STATE.OVERTIME_WORK; 386 | overtimeHours++; 387 | } else if (i < REGULAR_HOURS_PER_DAY) { 388 | currentState = STATE.REGULAR_WORK; 389 | } else if (i - REGULAR_HOURS_PER_DAY >= 2) { 390 | currentState = STATE.OVER_TWO_HOURS_WORK; 391 | overtimeHours++; 392 | } else if (i - REGULAR_HOURS_PER_DAY < 2) { 393 | currentState = STATE.OVERTIME_WORK; 394 | overtimeHours++; 395 | } else { 396 | currentState = STATE.OFF; 397 | } 398 | 399 | return currentState; 400 | }); 401 | workingMatrix.push(workday); 402 | 403 | if (dayOfWeek < 5) { 404 | let overtimeHours = workhour - 8; 405 | if (overtimeHours > 0 && overtimeHours <= 2) { 406 | pay += hourlyPay * 4 / 3 * overtimeHours; 407 | } else if (overtimeHours > 2) { 408 | pay += hourlyPay * 4 / 3 * 2 + hourlyPay * 5 / 3 * (overtimeHours - 2); 409 | } 410 | } 411 | 412 | if (dayOfWeek < 5 || ((dayOfWeek === 6 || dayOfWeek === 5) && reason !== 'disaster')) { 413 | overtimeHoursTotal += workhour - 8 > 0 ? workhour - 8 : 0; 414 | } 415 | }); 416 | var transposed = workingMatrix[0].map(function (col, i) { 417 | return workingMatrix.map(function (row) { 418 | return row[i]; 419 | }); 420 | }); 421 | 422 | // 週六與週日工作的薪資規則 423 | // 例假日工作八個小時以內,薪水皆以 hourlyPay * 8 計算 424 | // 超過八個小時薪水加倍發給 425 | [5, 6].forEach(day => { 426 | if (workhours[day] > 0 && workhours[day] <= 8) { 427 | pay += hourlyPay * 8; 428 | } else if (workhours[day] > 8) { 429 | if (reason === 'disaster') { 430 | pay += (workhours[day] - 8) * hourlyPay * 2 + hourlyPay * 8; 431 | } else if (workhours[day] <= 10) { 432 | pay += (workhours[day] - 8) * hourlyPay * 4 / 3 + hourlyPay * 8; 433 | } else if (workhours[day] > 10) { 434 | pay += (workhours[day] - 10) * hourlyPay * 5 / 3 + 2 * hourlyPay * 4 / 3 + hourlyPay * 8; 435 | } 436 | } 437 | }); 438 | 439 | let dayoff = 7; 440 | workhours.slice(0, 5).forEach(d => { 441 | if (d > 0) { 442 | dayoff--; 443 | } 444 | }); 445 | 446 | return { 447 | holiday: holiday, 448 | workingMatrix: workingMatrix, 449 | transposed: transposed, 450 | overtimePay: parseFloat(pay), 451 | overtimeHoursTotal: overtimeHoursTotal, 452 | totalWorkdays: 365 - holiday - dayoff * TOTAL_WEEK_PER_YEAR, 453 | extraDayoff: extraDayoff, 454 | illegal: illegal, 455 | illegalReason: illegalReason 456 | }; 457 | } 458 | 459 | export { 460 | REGULAR_HOURS_PER_DAY, 461 | DAY_NAMES, 462 | STATE, 463 | current, 464 | oneRestOneOff, 465 | twoOff 466 | }; 467 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import App from './App'; 4 | 5 | import Workweek from './components/Workweek'; 6 | import Faq from './components/Faq'; 7 | 8 | /* eslint-disable no-new */ 9 | Vue.use(VueRouter); 10 | 11 | let router = new VueRouter(); 12 | 13 | router.map({ 14 | '*': { 15 | name: 'workweek', 16 | component: Workweek 17 | }, 18 | '/': { 19 | name: 'workweek', 20 | component: Workweek 21 | }, 22 | '/faq': { 23 | name: 'faq', 24 | component: Faq 25 | } 26 | }); 27 | 28 | router.start(App, 'app'); 29 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0v/workweek/341a45c740e9bfad7095bb04ca83771196dd61e5/static/.gitkeep -------------------------------------------------------------------------------- /static/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0v/workweek/341a45c740e9bfad7095bb04ca83771196dd61e5/static/screenshot.png --------------------------------------------------------------------------------