Initial commit.

This commit is contained in:
Linus Miller 2016-08-14 13:27:48 +02:00
commit 256842e42d
53 changed files with 1963 additions and 0 deletions

16
.babelrc Normal file
View File

@ -0,0 +1,16 @@
{
"env": {
"server": {
"presets": [ "es2015-node6" ],
"plugins": [
"add-module-exports"
]
},
"client": {
"presets": [ "es2015", "react" ],
"plugins": [
"add-module-exports"
]
}
}
}

52
.eslintrc Normal file
View File

@ -0,0 +1,52 @@
{
"extends": "airbnb-base",
"parserOptions": {
"sourceType": "strict"
},
"env": {
"mocha": true
},
"rules": {
"semi": [ 2, "never" ],
"import/no-extraneous-dependencies": ["error", {"devDependencies": true, "optionalDependencies": false, "peerDependencies": false}],
"new-cap": 0,
"no-mixed-operators": 0,
"no-cond-assign": 0,
"guard-for-in": 0,
"global-require": 0,
"no-underscore-dangle": 0,
"object-shorthand": 0,
"default-case": 0,
"one-var": 0,
"prefer-rest-params": 0,
"no-unused-vars": [ 2, { "args": "none" } ],
"no-alert": 0,
"quote-props": 0,
"no-nested-ternary": 0,
"no-use-before-define": [2, { "functions": false, "classes": true }],
"consistent-return": 0,
"no-eval": 0,
"prefer-arrow-callback": 0,
"array-bracket-spacing": 0,
"no-console": 0,
"indent": [ 2, 2, { "SwitchCase": 1 }],
"max-len": 0,
"comma-dangle": 0,
"no-param-reassign": 0,
"prefer-template": 0,
"curly": 0,
"func-names": 0,
"no-shadow": 0,
"spaced-comment": 0,
"strict": [ 2, "global" ]
},
"globals": {
"PWD": true,
"ENV": true,
"location": true
}
}

40
.gitignore vendored Normal file
View File

@ -0,0 +1,40 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# NPM Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
npm-debug.log
# Bower dependency directory
bower_components
*.marko.js
/dump
/public
/server/uploads

158
README.md Normal file
View File

@ -0,0 +1,158 @@
# MyPaper
## Running
1. `$ git clone git@gitlab.thecodebureau.com:lohfu/pomodoro.git`
2. `$ cd pomodoro`
2. `$ npm install`
3. `$ npm run gulp`
Navigate to localhost:1337 (Node app runs on the port given in
`server/config/port`, but Gulp starts a
[BrowserSync](https://github.com/BrowserSync/browser-sync) proxy that reloads
your browser automatically when you change certain files).
## Building (Production environment)
Simply call `$ npm run gulp:production` to run all build tasks in
production mode, without starting the server.
## Routes
## Files & Directories
### /img/raster
All raster images. Will be symlinked into `/public/img`.
### /img/svg
All SVG images. Will be symlinked into `/public/img` in development ENV,
and minified to the same location in production ENV.
### /gulpfile.js
Runs the code in `/gulp`.
### /gulp
This contains ALL gulp logic.
#### /gulp/config.js
This file contains all the configuration for the gulp tasks. Editing
this file should be enough for most needs.
### /less
This folder contains all SASS files and is compiled with the gulp sass task,
which puts the output CSS into `/public/css`
### /public
This directory is maintained by gulp. All static assets get symlinked in here,
and any built code
This folder should not be edited directly, or commited to git. It is
removed everytime gulp initializes.
### /server
This directory contains all JavaScript that only has to do with running your
server instance.
### /server/middleware
Generic middleware that do not belong to a specific service.
Should export a single middleware function, or a namespaced object of
functions.
### /server/models
Shared models or models that do not belong to a specific service.
Should export the initialized model, ie:
```
module.exports = new mongoose.Model('Model', ModelSchema);
```
### /server/routes
Routes that do not belong to a server.
Should return an array of route arrays (`[ path, method, [ mw ] ]`)routes:
```
module.exports = [
[ '/', 'get', [ mw.one, mw.two ] ],
[ '/page', 'get', [ mw.three, mw.four ] ],
[ '/page2', 'get', [ mw.five, mw.eight ] ]
];
```
### /server/config
All configuration for the server. Most files export different options
depending on the environment. A file might look like this:
```
module.exports = {
development: {},
testing: {},
staging: {},
production: {}
}[ENV];
```
If you want defaults applied, you might do something like this:
```
const defaults = {}
module.exports = Object.assign({}, defaults, {
production: {}
}[ENV]);
```
In which case defaults will be used for all environments except
`production`, in which the production settings will override
any settings in defaults.
If you don't want to override, do this instead:
```
const defaults = {}
module.exports = {
production: {}
}[ENV] || defaults);
```
### /server/services
Instead of splitting models, middlewares and routes into three seperate
folders, we create service directories that contain files relating
to the same functionality.
### /server/templates
Marko templates.
### /src
This contains all isomorphic and pure browser JavaScript.
Browserify bundles these files (entry point is `app.js`) and outputs
`public/app.js`.
### /static
This contains all static content. All files in here are symlinked into
`/public`, while retaining its relative path. IE `/static/fonts/font.otf` gets
symlinked to `/public/fonts/font.otf`.
The only special files are robots.txt. In all ENV besides production
`robots.txt` will be symlinked, in production environment
`robots-production.txt` will be used instead.

47
bin/create-roles.js Executable file
View File

@ -0,0 +1,47 @@
#!/bin/env node
'use strict';
const p = require('path');
const chalk = require('chalk');
const MongoClient = require('mongodb').MongoClient;
const successPrefix = '[' + chalk.green('SUCCESS') + '] ';
const errorPrefix = '[' + chalk.red('ERROR') + '] ';
global.PWD = p.dirname(__dirname);
global.ENV = process.env.NODE_ENV || 'development';
console.log(PWD);
function _mongo(collection, cb) {
const mongoConfig = require(p.join(PWD, 'server/config/mongo'));
MongoClient.connect(mongoConfig.uri, function (err, db) {
if (err) {
console.error(errorPrefix);
console.error(err);
process.exit(1);
}
cb(db.collection(collection), db);
});
}
function createRoles(roles) {
const roleNames = (roles ? roles.split(',') : [ 'user', 'admin' ]);
_mongo('roles', function (roles, db) {
roles.insert(roleNames.map(function (role) { return { name: role }; }), function (err) {
db.close();
if (err) {
console.error(errorPrefix);
console.error(err);
process.exit(1);
} else {
console.log(successPrefix + 'Created roles: ' + roleNames.join(', '));
process.exit(0);
}
});
});
}
createRoles();

98
bin/create-user.js Executable file
View File

@ -0,0 +1,98 @@
#!/bin/env node
'use strict';
const p = require('path');
const crypto = require('crypto');
const chalk = require('chalk');
const MongoClient = require('mongodb').MongoClient;
const successPrefix = '[' + chalk.green('SUCCESS') + '] ';
const errorPrefix = '[' + chalk.red('ERROR') + '] ';
global.PWD = p.dirname(__dirname);
global.ENV = process.env.NODE_ENV || 'development';
function _mongo(collection, cb) {
const mongoConfig = require(p.join(PWD, 'server/config/mongo'));
MongoClient.connect(mongoConfig.uri, function (err, db) {
if (err) {
console.error(errorPrefix);
console.error(err);
process.exit(1);
}
cb(db.collection(collection), db);
});
}
const SALT_LENGTH = 16;
function _hash(password) {
// generate salt
password = password.trim();
const chars = '0123456789abcdefghijklmnopqurstuvwxyz';
let salt = '';
for (let i = 0; i < SALT_LENGTH; i++) {
const j = Math.floor(Math.random() * chars.length);
salt += chars[j];
}
// hash the password
const passwordHash = crypto.createHash('sha512').update(salt + password).digest('hex');
// entangle the hashed password with the salt and save to the model
return _entangle(passwordHash, salt, password.length);
}
function _entangle(string, salt, t) {
string = salt + string;
const length = string.length;
const arr = string.split('');
for (let i = 0; i < salt.length; i++) {
const num = ((i + 1) * t) % length;
const tmp = arr[i];
arr[i] = arr[num];
arr[num] = tmp;
}
return arr.join('');
}
function createUser(email, password, roles) {
if (!email || !password) {
console.log('Usage: bin/create-user.js [email] [password] [?roles]');
process.exit(1);
}
const user = {
email: email,
local: {
password: _hash(password)
},
roles: roles ? roles.split(',') : [ 'admin' ],
isVerified: true,
dateCreated: new Date()
};
_mongo('users', function (users, db) {
users.insert(user, function (err, user) {
db.close();
if (err) {
console.error(errorPrefix);
console.error(err);
process.exit(1);
} else {
console.log(successPrefix + 'Saved user: ' + user.ops[0].email);
process.exit(0);
}
});
});
}
createUser.apply(null, process.argv.slice(2));

1
doc/ideas.md Normal file
View File

@ -0,0 +1 @@
+ timer in document title (so you can see it in the browser tab)

22
gulp/LICENSE Normal file
View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2016 Linus Miller
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

23
gulp/README.md Normal file
View File

@ -0,0 +1,23 @@
# gulp
## Tasks
### browserify.js
### browser-sync.js
### fonts.js
### nodemon.js
### raster.js
### sass.js
### static.js
### svg.js
### watch.js
### wipe.js

121
gulp/config.js Normal file
View File

@ -0,0 +1,121 @@
'use strict'
const p = require('path')
const fs = require('fs')
const port = fs.existsSync(p.join(PWD, 'server/config/port.js')) ?
require(p.join(PWD, 'server/config/port')) : 10000
module.exports = {
browserSync: {
browser: null,
ghostMode: false,
proxy: 'localhost:' + port,
port: 1337,
ui: {
port: 1338
},
files: [
p.join(PWD, 'public/css/**/*.css'),
p.join(PWD, 'public/js/**/*.js'),
]
},
browserify: {
suffix: true,
debug: true,
dest: p.join(PWD, 'public/js'),
extensions: [ '.js', '.jsx', '.json' ],
// PWD/node_modules is added so symlinked ridge does not break. used to
// work without this in browserify 9
paths: [ p.join(PWD, 'node_modules'), p.join(PWD, 'modules') ],
// outputs only need to be used if output names are different from entries.
// Otherwise the entries array is copied into the outputs array.
entries: [
'src/app.js'
],
src: p.join(PWD, 'src')
},
less: {
suffix: true,
src: [
p.join(PWD, 'less/*.less'),
],
dest: p.join(PWD, 'public/css'),
autoprefixer: {
browsers: [
'safari >= 5',
'ie >= 8',
'ios >= 6',
'opera >= 12.1',
'firefox >= 17',
'chrome >= 30',
'android >= 4'
],
cascade: true
},
functions: require('./less/functions'),
options: {
paths: [
p.join(PWD, 'node_modules/spineless/less')
],
}
},
nodemon: {
ext: 'js, marko',
watch: [
'server',
'src',
],
ignore: [
'*.marko.js',
// only watch marko files in src folders.
'src/**/*.js'
],
script: fs.existsSync(p.join(PWD, 'package.json')) ? require(p.join(PWD, 'package.json')).main.replace(/^\./, PWD) : 'server/server.js',
env: {
BABEL_ENV: 'server',
// what port you actually put into the browser... when using browser-sync
// this will differ from the internal port used by express
EXTERNAL_PORT: 1337,
// needed to force debug to use colors despite tty.istty(0) being false,
// which it is in a child process
DEBUG_COLORS: true,
// needed to force chalk to use when running gulp nodemon tasks.
FORCE_COLOR: true,
PWD,
NODE_ENV: ENV,
DEBUG: 'mypaper:*'
}
},
raster: {
src: p.join(PWD, 'img/raster/**/*.{png,gif,jpg}'),
dest: p.join(PWD, 'public/img')
},
static: {
src: p.join(PWD, 'static/**/*'),
dest: p.join(PWD, 'public')
},
svg: {
src: p.join(PWD, 'img/svg/**/*.svg'),
dest: p.join(PWD, 'public/img')
},
tasks: {
development: [ 'wipe', [ 'browserify', 'less', 'raster', 'static', 'svg' ], [ 'nodemon' ], [ 'watch', 'browser-sync' ] ],
production: [ 'wipe', [ 'browserify', 'less', 'raster', 'static', 'svg' ]]
}[ENV],
watch: {
less: p.join(PWD, 'less/**/*.less')
},
wipe: {
src: [ p.join(PWD, 'public') ]
}
}

30
gulp/index.js Normal file
View File

@ -0,0 +1,30 @@
'use strict'
// modules > 3rd party
const _ = require('lodash')
// modules > gulp
const gulp = require('gulp')
global.ENV = process.env.NODE_ENV || 'development'
global.PWD = process.env.PWD
const args = process.argv.slice(2)
// use tasks from arguments list if present, otherwise use tasks from
// configuration (environment specific)
let tasks = args.length > 0 ? args : require('./config').tasks
// only require used tasks
_.flatten(tasks, true).forEach((task) => require('./tasks/' + task))
tasks = tasks.map((task) => {
if (Array.isArray(task)) {
return gulp.parallel(...task)
}
return task
})
// set up the 'default' task to use runSequence to run all tasks
gulp.task('default', gulp.series(...tasks))

42
gulp/less/functions.js Normal file
View File

@ -0,0 +1,42 @@
'use strict'
const p = require('path')
module.exports = {
'img-url': function (value) {
const tree = this.context.pluginManager.less.tree
return new tree.URL(new tree.Quoted('"', p.join('/img', value.value)), this.index, this.currentFileInfo)
},
rem: function (value, context) {
const tree = this.context.pluginManager.less.tree
if (value.type === 'Expression')
return new tree.Expression(value.value.map((value) => {
if (value.unit.backupUnit === 'px')
return new tree.Dimension(value.value / 14, 'rem')
return new tree.Dimension(value.value, 'rem')
}))
return new tree.Dimension(value.value / 14, 'rem')
},
px: function (value) {
const tree = this.context.pluginManager.less.tree
if (value.type === 'Expression')
return new tree.Expression(value.value.map((value) => {
if (value.unit.backupUnit === 'px')
return value
return new tree.Dimension(value.value * 14, 'px')
}))
if (value.unit.backupUnit === 'px')
return value
return new tree.Dimension(value.value * 14, 'px')
}
}

View File

@ -0,0 +1,9 @@
'use strict'
const gulp = require('gulp')
const browserSync = require('browser-sync')
const TASK_NAME = 'browser-sync'
const config = require('../config').browserSync
gulp.task(TASK_NAME, () => browserSync(config))

126
gulp/tasks/browserify.js Normal file
View File

@ -0,0 +1,126 @@
'use strict'
// native modules
const p = require('path')
const fs = require('fs')
// 3rd party modules
const _ = require('lodash')
const browserify = require('browserify')
const buffer = require('vinyl-buffer')
const chalk = require('chalk')
const es = require('event-stream')
const gulp = require('gulp')
const gutil = require('gulp-util')
const rename = require('gulp-rename')
const source = require('vinyl-source-stream')
const sourcemaps = require('gulp-sourcemaps')
const uglify = require('gulp-uglify')
const watchify = require('watchify')
const babelify = require('babelify')
const markoify = require('markoify')
require('marko/compiler').defaultOptions.writeToDisk = false
process.env.BABEL_ENV = 'client'
const config = require('../config').browserify
const suffix = config.suffix && ENV === 'production' ? '-' + Date.now().toString(16) : ''
const errorHandler = require('../util/error-handler')
function formatError(err) {
err.task = 'browserify'
const matchFileName = err.message.match(/\/[^\s:]*/)
if (matchFileName) {
let fileName = matchFileName[0]
if (fileName.indexOf(PWD) > -1)
fileName = './' + p.relative(PWD, fileName)
fileName = chalk.yellow(fileName)
const matchNumbers = err.message.match(/ \((.*)\)/)
if (matchNumbers) {
const arr = matchNumbers[1].split(':')
err.message = err.message.slice(0, matchNumbers.index) +
' at line ' + arr[0] + ', col ' + arr[1]
}
err.message = err.message.split(matchFileName[0]).join(fileName)
}
err.message = err.message.split(/:\s*/).join('\n')
errorHandler.call(this, err)
}
if (typeof config.entries === 'string') config.entries = [config.entries]
const TASK_NAME = 'browserify'
gulp.task(TASK_NAME, (cb) => {
cb = _.after(config.entries.length, cb)
if (config.suffix)
fs.writeFileSync(config.dest + '.json', JSON.stringify({ suffix }))
const tasks = config.entries.map((entry, index) => {
function bundler(bundle) {
let outputPath = config.outputs && config.outputs[index] || entry
let pipe = bundle.bundle()
.on('error', formatError)
.pipe(source(outputPath))
.pipe(rename((path) => {
if (outputPath === entry) {
// remove first level of directores, eg `src/` or `src-widget/`
path.dirname = path.dirname.replace(/^src[^\/]*\/?/, '')
outputPath = path.dirname + path.basename + (path.suffix || '') + path.extname
}
path.basename += suffix
return path
}))
.on('end', (...args) => {
gutil.log(chalk.cyan(TASK_NAME) + ' wrote ' + chalk.magenta(outputPath) + '.')
cb()
})
if (ENV !== 'development')
pipe = pipe.pipe(buffer())
.pipe(sourcemaps.init({ loadMaps: true }))
.pipe(uglify())
.pipe(sourcemaps.write('./'))
return pipe.pipe(gulp.dest(config.dest))
}
const bundle = browserify(_.defaults({
entries: [ entry ],
cache: {},
packageCache: {}
}, config))
if (ENV !== 'production')
bundle.plugin(watchify, {
ignoreWatch: [ '/public/**', '**/node_modules/**', '**/bower_components/**']
})
bundle.transform(babelify)
bundle.transform(markoify)
bundle.on('update', () => bundler(bundle))
return bundler(bundle)
})
// return a single stream from all bundle streams
return es.merge.apply(null, tasks)
})

49
gulp/tasks/less.js Normal file
View File

@ -0,0 +1,49 @@
'use strict'
const fs = require('fs')
const gulp = require('gulp')
const _less = require('less')
const less = require('gulp-less')
const sourcemaps = require('gulp-sourcemaps')
const rename = require('gulp-rename')
const mkdirp = require('mkdirp')
const postcss = require('gulp-postcss')
const autoprefixer = require('autoprefixer')
const config = require('../config').less
const errorHandler = require('../util/error-handler')
_less.functions.functionRegistry.addMultiple(config.functions)
const processors = [
autoprefixer(config.autoprefixer),
]
if (ENV === 'production') {
const csswring = require('csswring')
processors.push(csswring(config.csswring))
}
const suffix = config.suffix && ENV === 'production' ? '-' + Date.now().toString(16) : undefined
gulp.task('less', function () {
mkdirp(config.dest)
if (suffix)
fs.writeFile(config.dest + '.json', JSON.stringify({ suffix }))
let pipe = gulp.src(config.src)
.pipe(sourcemaps.init())
.pipe(less(config.options).on('error', errorHandler))
.pipe(postcss(processors))
if (suffix)
pipe = pipe.pipe(rename({ suffix }))
return pipe.pipe(sourcemaps.write('./maps'))
.pipe(gulp.dest(config.dest))
})

35
gulp/tasks/nodemon.js Normal file
View File

@ -0,0 +1,35 @@
'use strict'
const _ = require('lodash')
const gulp = require('gulp')
const browserSync = require('browser-sync')
const nodemon = require('nodemon')
const config = require('../config').nodemon
// for some reason, this was needed somewhere before
//process.stdout.isTTY = true
gulp.task('nodemon', (cb) => {
nodemon(_.defaults({ stdout: false }, config))
.on('log', function (log) {
console.log(log.colour)
})
.on('readable', function () {
this.stdout.pipe(process.stdout)
this.stderr.pipe(process.stderr)
this.stdout.on('data', (chunk) => {
if (/HTTP server (running|listening|started) on|at port/i.test(chunk.toString('utf-8').trim())) {
if (cb)
cb()
cb = null
if (browserSync.active)
browserSync.reload()
}
})
})
})

27
gulp/tasks/raster.js Normal file
View File

@ -0,0 +1,27 @@
'use strict'
// modules > native
const p = require('path')
// modules > 3rd party
const chalk = require('chalk')
// modules > gulp:utilities
const gulp = require('gulp')
const gutil = require('gulp-util')
const TASK_NAME = 'raster'
const config = require('../config').raster
gulp.task(TASK_NAME, () => {
let count = 0
return gulp.src(config.src)
.pipe(gulp.symlink((file) => {
count++
return p.join(config.dest)
}, { log: false }))
.on('end', () => {
gutil.log(chalk.cyan(TASK_NAME) + ' done symlinking ' + chalk.bold.blue(count) + ' files')
})
})

39
gulp/tasks/static.js Normal file
View File

@ -0,0 +1,39 @@
'use strict'
// modules > native
const p = require('path')
const fs = require('fs')
// modules > 3rd party
const chalk = require('chalk')
// modules > gulp:utilities
const gulp = require('gulp')
const through = require('through2')
const gutil = require('gulp-util')
const TASK_NAME = 'static'
const config = require('../config').static
gulp.task(TASK_NAME, () => {
let count = 0
return gulp.src(config.src)
// we use through so that we can skip directories
// we skip directories because we want to merge file structure
.pipe(through.obj((file, enc, callback) => {
fs.stat(file.path, (err, stats) => {
if (stats.isDirectory())
file = null
callback(null, file)
})
}))
.pipe(gulp.symlink((file) => {
count++
return p.join(config.dest)
}))
.on('end', () => {
gutil.log(chalk.cyan(TASK_NAME) + ' done symlinking ' + chalk.bold.blue(count) + ' files')
})
})

27
gulp/tasks/svg.js Normal file
View File

@ -0,0 +1,27 @@
'use strict'
// modules > native
const p = require('path')
// modules > 3rd party
const chalk = require('chalk')
// modules > gulp:utilities
const gulp = require('gulp')
const gutil = require('gulp-util')
const TASK_NAME = 'svg'
const config = require('../config').svg
gulp.task(TASK_NAME, () => {
let count = 0
return gulp.src(config.src)
.pipe(gulp.symlink((file) => {
count++
return p.join(config.dest)
}))
.on('end', () => {
gutil.log(chalk.cyan(TASK_NAME) + ' done symlinking ' + chalk.bold.blue(count) + ' files')
})
})

16
gulp/tasks/watch.js Normal file
View File

@ -0,0 +1,16 @@
'use strict'
// modules > 3rd party
const _ = require('lodash')
// modules > gulp:utilities
const gulp = require('gulp')
const TASK_NAME = 'watch'
const config = require('../config').watch
gulp.task(TASK_NAME, () => {
_.forIn(config, (value, key) => {
gulp.watch(value, gulp.series(key))
})
})

44
gulp/tasks/wipe.js Normal file
View File

@ -0,0 +1,44 @@
'use strict'
// native modules
const fs = require('fs')
// 3rd party modules
const mkdirp = require('mkdirp')
const chalk = require('chalk')
const gulp = require('gulp')
const gutil = require('gulp-util')
const rimraf = require('rimraf')
const TASK_NAME = 'wipe'
const config = require('../config').wipe
gulp.task(TASK_NAME, (cb) => {
let count = 0
config.src.forEach((folder) => {
fs.exists(folder, (exists) => {
if (exists) {
rimraf(folder, (err) => {
if (err) throw err
gutil.log('Folder ' + chalk.magenta(folder) + ' removed')
mkdirp.sync(folder)
count++
if (count >= config.src.length) {
cb()
}
})
} else {
count++
mkdirp.sync(folder)
if (count >= config.src.length) {
cb()
}
}
})
})
})

View File

@ -0,0 +1,16 @@
'use strict'
const util = require('gulp-util')
const chalk = require('chalk')
module.exports = function (err) {
util.log(chalk.red('ERROR') + (err.task ? ' in task \'' + chalk.cyan(err.task) : ' in plugin \'' + chalk.cyan(err.plugin)) + '\'')
console.log('\n' + err.message.trim() + '\n')
// needed for error handling not thrown by gulp-watch
if (this.emit) {
// Keep gulp from hanging on this task
this.emit('end')
}
}

7
gulpfile.js Normal file
View File

@ -0,0 +1,7 @@
'use strict';
require('./gulp');
process.on('SIGINT', () => {
process.exit(0);
});

0
less/main.less Normal file
View File

79
package.json Normal file
View File

@ -0,0 +1,79 @@
{
"name": "budbilar",
"version": "0.0.1",
"description": "När du vill få något budat till rätt pris.",
"main": "server/server.js",
"private": true,
"repository": {
"type": "git",
"url": "git@gitlab.thecodebureau.com:axel/budbilar.git"
},
"scripts": {
"gulp": "gulp",
"gulp:production": "NODE_ENV=production gulp",
"gulp:development": "NODE_ENV=development gulp",
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server/server.js"
},
"dependencies": {
"babel-preset-es2015-node6": "^0.3.0",
"babel-register": "^6.11.6",
"body-parser": "^1.15.2",
"chalk": "^1.1.3",
"connect-redis": "^3.1.0",
"cookie-parser": "^1.4.3",
"dollr": "0.0.7",
"express": "^4.14.0",
"express-module-membership": "github:thecodebureau/express-module-membership",
"express-service-errors": "github:thecodebureau/express-service-errors",
"express-session": "^1.14.0",
"lodash": "^4.15.0",
"marko": "^3.7.2",
"marko-widgets": "^6.3.3",
"mongoose": "^4.5.8",
"mongopot": "github:lohfu/mongopot",
"morgan": "^1.7.0",
"nodemailer": "^2.5.0",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
"require-dir": "^0.3.0",
"warepot": "github:lohfu/warepot"
},
"devDependencies": {
"autoprefixer": "^6.4.0",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1",
"babelify": "^7.3.0",
"browser-sync": "^2.14.0",
"browserify": "^13.1.0",
"chalk": "^1.1.3",
"csswring": "^5.1.0",
"eslint": "^3.3.0",
"eslint-config-airbnb": "^10.0.1",
"eslint-config-airbnb-base": "^5.0.2",
"eslint-plugin-import": "^1.13.0",
"eslint-plugin-jsx-a11y": "^2.1.0",
"eslint-plugin-react": "^6.0.0",
"event-stream": "^3.3.4",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-less": "^3.1.0",
"gulp-postcss": "^6.1.1",
"gulp-rename": "^1.2.2",
"gulp-sourcemaps": "^1.6.0",
"gulp-svgmin": "^1.2.2",
"gulp-uglify": "^2.0.0",
"gulp-util": "^3.0.7",
"marko": "^3.9.4",
"markoify": "^2.1.1",
"mkdirp": "^0.5.1",
"nodemon": "^1.10.0",
"postcss": "^5.1.2",
"pretty-hrtime": "^1.0.2",
"rimraf": "^2.5.4",
"through2": "^2.0.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.7.0"
}
}

7
server/config/dir.js Normal file
View File

@ -0,0 +1,7 @@
'use strict'
const p = require('path')
module.exports = {
static: p.join(PWD, 'public'),
}

View File

@ -0,0 +1,58 @@
'use strict'
const _ = require('lodash')
const errorTemplate = require('../templates/error.marko')
const defaults = {
post: (req, res, next) => {
res.template = errorTemplate
next()
},
mystify: {
properties: ['errors', 'message', 'name', 'status', 'statusText']
},
log: {
// if database = true there has to be a mongoose model name ErrorModel
ignore: [],
}
}
const ErrorModel = require('mongopot/models/error')
function store(error) {
ErrorModel.create(error, (err) => {
// TODO handle errors in error handler better
if (err) {
console.error('ERROR WRITING TO DATABASE')
console.error(err)
console.log(err.errors)
console.error('ORIGINAL ERROR')
console.error(error)
}
})
}
module.exports = _.merge(defaults, {
development: {
log: {
store: store,
console: true,
}
},
testing: {
log: {
store: false,
console: false,
},
},
production: {
log: {
store: store,
console: false,
}
},
}[ENV])

4
server/config/globals.js Normal file
View File

@ -0,0 +1,4 @@
'use strict'
global.LOGIN_USER = 'lohfu@lohfu.io'
//global.LOGIN_USER = 'zarac@zarac.se'

View File

@ -0,0 +1,69 @@
'use strict'
const config = {
smtp: require('./smtp'),
site: require('./site')
}
module.exports = {
invite: {
from: config.site.title + ' Robot <' + config.site.emails.robot + '>',
subject: 'You have been invited to ' + config.site.title
},
paths: {
register: '/register',
login: '/login',
forgotPassword: '/forgot-password',
updatePassword: '/update-password'
},
remember: {
// if expires is defined, it will be used. otherwise maxage
expires: new Date('2038-01-19T03:14:07.000Z'),
//expires: Date.now() - 1,
maxAge: 30 * 24 * 60 * 60 * 1000,
},
messages: {
login: {
notLocal: 'Account requires external login.',
wrongPassword: 'Wrong password.',
noLocalUser: 'No user registered with that email.',
noExternalUser: 'The account is not connected to this website.',
externalLoginFailed: 'External login failed.',
unverified: 'This account has not been verified.',
banned: 'User is banned.',
blocked: 'User is blocked due to too many login attempts.'
},
register: {
notAuthorized: 'The email is not authorized to create an account.',
duplicateEmail: 'The email has already been registered.'
}
},
passport: {
local: {
usernameField: 'email'
},
scope: [ 'email' ],
//providers: {
// facebook: {
// clientID: 'change-this-fool',
// clientSecret: 'change-this-fool',
// callbackURL: p.join(config.site.domain, '/auth/facebook/callback'),
// passReqToCallback: true
// },
// google: {
// clientID: 'change-this-fool',
// clientSecret: 'change-this-fool',
// callbackURL: p.join(config.site.domain, '/auth/google/callback'),
// passReqToCallback: true
// }
//}
}
}

11
server/config/mongo.js Normal file
View File

@ -0,0 +1,11 @@
'use strict'
const defaults = {
uri: 'mongodb://pomodoro-supreme:lets-work-our-asses-off-poop-poop@mongo.thecodebureau.com/pomodoro'
}
module.exports = Object.assign(defaults, {
production: {
//uri: 'mongodb://pomodoro-supreme:lets-work-our-asses-off-poop-poop@localhost/pomodoro'
}
}[ENV])

10
server/config/port.js Normal file
View File

@ -0,0 +1,10 @@
'use strict'
const basePort = 3050
module.exports = {
development: basePort,
testing: basePort + 1,
staging: basePort + 2,
production: basePort + 3
}[ENV]

37
server/config/session.js Normal file
View File

@ -0,0 +1,37 @@
'use strict'
const session = require('express-session')
let redisStore
const config = {
secret: 'asdf1h918798&(*&ijh21kj4hk123j45h2k34jh52k3g45)thisisacompletelyrandomgeneratedstring...whatastrangecoincidence...',
resave: false,
saveUninitialized: true
}
const redisConfig = {
host: 'localhost',
port: 6379
}
if (ENV === 'production') {
const RedisStore = require('connect-redis')(require('express-session'))
redisStore = new RedisStore(redisConfig)
redisStore.on('connect', function () {
console.info('Redis connected succcessfully')
})
redisStore.on('disconnect', function () {
throw new Error('Unable to connect to redis. Has it been started?')
})
config.store = redisStore
} else {
config.store = new session.MemoryStore()
}
module.exports = config

51
server/config/site.js Normal file
View File

@ -0,0 +1,51 @@
'use strict'
const _ = require('lodash')
const domain = 'pomodoro.bitmill.co'
const defaults = {
domain: domain,
title: 'Pomodoro',
name: 'pomodoro',
protocol: 'http',
get host() {
return this.port ? this.hostname + ':' + this.port : this.hostname
},
get url() {
return this.protocol + '://' + this.host + '/'
},
emails: {
robot: 'no-reply@thecodebureau.com',
info: 'info@thecodebureau.com',
webmaster: 'webmaster@thecodebureau.com',
order: 'info@thecodebureau.com'
}
}
module.exports = _.merge(defaults, {
development: {
hostname: 'localhost',
port: process.env.EXTERNAL_PORT || process.env.PORT || require('./port')
},
testing: {
hostname: 'localhost',
port: process.env.PORT || require('./port')
},
staging: {
hostname: 'staging.' + domain
},
production: {
hostname: domain,
protocol: 'https',
emails: {
robot: 'no-reply@bitmill.co',
info: 'info@bitmill.co',
webmaster: 'webmaster@bitmill.co',
order: 'order@bitmill.co'
}
}
}[ENV])

15
server/config/smtp.js Normal file
View File

@ -0,0 +1,15 @@
'use strict'
const _ = require('lodash')
const defaults = {
auth: {
user: 'SMTP_Injection',
// dev key
pass: '2eec390c5b3f5d593c9f152179bf51e90b073784'
},
host: 'smtp.sparkpostmail.com',
port: 587
}
module.exports = _.merge(defaults, {}[ENV])

11
server/pages.js Normal file
View File

@ -0,0 +1,11 @@
'use strict'
const masterTemplate = require('../src/master.marko')
module.exports = [
[ '/', 'get', (req, res, next) => {
res.template = masterTemplate
next()
} ],
]

130
server/server.js Normal file
View File

@ -0,0 +1,130 @@
'use strict'
require('babel-register')
/*
* The main file that sets up the Express instance and node
*
* @module server/server
* @type {Express instance}
*/
// set up some globals (these are also set in Epiphany if not already set)
global.ENV = process.env.NODE_ENV || 'development'
global.PWD = process.env.NODE_PWD || process.cwd()
// make node understand `*.marko` files
require('marko/node-require').install()
// modules > native
const p = require('path')
// modules > 3rd party
const requireDir = require('require-dir')
const _ = require('lodash')
const express = require('express')
const mongoose = require('mongoose')
const passport = require('passport')
// modules > express middlewares
const bodyParser = require('body-parser')
const session = require('express-session')
const cookieParser = require('cookie-parser')
// modules > 3rd party
const chalk = require('chalk')
// modules > warepot
const colorizeStack = require('warepot/util/colorize-stack')
// modules > locals
const initRoutes = require('warepot/util/init-routes')
const config = requireDir('./config')
// make error output stack pretty
process.on('uncaughtException', (err) => {
console.error(chalk.red('UNCAUGHT EXCEPTION'))
if (err.stack) {
console.error(colorizeStack(err.stack))
} else {
console.error(err)
}
process.exit(1)
})
const prewares = [
express.static(config.dir.static, ENV === 'production' ? { maxAge: '1 year' } : null),
bodyParser.json(),
bodyParser.urlencoded({ extended: true }),
cookieParser(),
session(config.session),
passport.initialize(),
passport.session(),
]
if (ENV === 'development') {
// only log requests to console in development mode
prewares.unshift(require('morgan')('dev'))
prewares.push(require('express-module-membership/passport/automatic-login'))
}
const postwares = [
require('warepot/ensure-found'),
// transform and log error
require('warepot/error-handler'),
// respond
require('warepot/responder'),
// handle error rendering error
require('warepot/responder-error'),
]
const routes = [
...prewares,
...require('./pages'),
...require('./services/pomodoros/routes'),
...require('express-module-membership/routes'),
...postwares
]
const server = express()
// get IP & whatnot from nginx proxy
server.set('trust proxy', true)
server.locals.site = require('./config/site')
if (ENV === 'production') {
Object.assign(server.locals, {
js: require(p.join(PWD, 'public/js.json')),
css: require(p.join(PWD, 'public/css.json'))
})
}
// override default response render method for
// more convenient use with marko
server.response.render = function (template) {
const locals = _.extend({}, server.locals, this.locals)
template.render(locals, this)
}
initRoutes(server, routes)
// mongoose mpromise library is being deprecated
mongoose.Promise = Promise
// connect to mongodb
mongoose.connect(config.mongo.uri, _.omit(config.mongo, 'uri'), (err) => {
if (err) {
console.error(err)
process.exit()
}
console.info('[' + chalk.cyan('INIT') + '] Mongoose is connected.')
})
server.listen(config.port, () => {
console.info('[' + chalk.cyan('INIT') + '] HTTP Server listening on port ' + chalk.magenta('%s') + ' (' + chalk.yellow('%s') + ')', config.port, ENV)
})
module.exports = server

View File

@ -0,0 +1,72 @@
'use strict'
const _ = require('lodash')
const formatQuery = require('warepot/format-query')
const Pomodoro = require('./model')
function create(req, res, next) {
Pomodoro.create(Object.assign(req.body, { user: req.user && req.user.id || '57b04f50a1eaaf354f3b96a6' }), (err, pomodoro) => {
res.locals.pomodoro = pomodoro
next(err)
})
}
function find(req, res, next) {
const limit = Math.max(0, req.query.limit) || res.locals.limit
const query = Pomodoro.find(_.omit(req.query, 'limit', 'sort', 'page'),
null,
{ sort: req.query.sort || '-startDate', lean: true })
if (limit)
query.limit(limit)
query.exec((err, pomodoros) => {
res.locals.pomodoros = pomodoros
next(err)
})
}
function end(req, res, next) {
Pomodoro.findByIdAndUpdate(req.params.id, { endDate: new Date() }, (err, pomodoro) => {
if (err) return next(err)
res.locals.pomodoro = pomodoro
next()
})
}
function getActive(req, res, next) {
Pomodoro.find({ user: req.user.id, endDate: { $eq: null } }, (err, pomodoros) => {
res.locals.pomodoros = pomodoros
return next(err)
})
}
function patch(req, res, next) {
Pomodoro.findByIdAndUpdate(req.params.id, req.body, (err, pomodoro) => {
if (err) return next(err)
res.locals.pomodoro = pomodoro
next()
})
}
module.exports = {
create,
end,
find,
formatQuery: formatQuery([ 'limit', 'sort' ], {
endDate: 'exists'
}),
getActive,
patch
}

View File

@ -0,0 +1,23 @@
'use strict'
const mongoose = require('mongoose')
const PomodoroSchema = new mongoose.Schema({
startTime: {
type: Date,
required: true
},
endTime: {
type: Date,
required: true
},
name: String,
location: String,
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
}
})
module.exports = mongoose.model('Pomodoro', PomodoroSchema)

View File

@ -0,0 +1,13 @@
'use strict'
//const isAuthenticated = require('express-module-membership/passport/authorization-middleware').isAuthenticated;
const isAuthenticated = (req, res, next) => next()
const mw = require('./middleware')
module.exports = [
[ '/api/pomodoros', 'post', [ isAuthenticated, mw.create ]],
[ '/api/pomodoros/:id', 'patch', [ isAuthenticated, mw.patch ]],
//[ '/api/pomodoros', 'get', [ mw.authorization.isAuthenticated, mw.pomodoros.getActive ]]
[ '/api/pomodoros', 'get', [ isAuthenticated, mw.formatQuery, mw.find ]]
]

View File

@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<title>Newseri - ERROR</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
</head>
<body>
<div id="content">
<h1>Error ${data.error.status}</h1>
<pre>${JSON.stringify(data.error, null, ' ')}</pre>
</div>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!doctype html>
<html>
<head>
<title>Budbilar</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
</head>
<body>
<h1>Välkommen till Budbilar</h1>
<nav>
<li><a href="/">Start</a></li>
</nav>
<p>Lorem ipsum dolizzle sit sizzle, things adipiscing elit. Nullizzle
sapizzle velizzle, for sure volutpizzle, suscipit quis, boofron vizzle, pot.
Pellentesque its fo rizzle tortizzle. Sed erizzle. Fusce izzle dolor brizzle we
gonna chung pot ass. Maurizzle yo crunk et turpizzle. Gangsta izzle tortizzle.
Pellentesque eleifend rhoncizzle its fo rizzle. In yippiyo mammasay mammasa
mamma oo sa pizzle dictumst. Boofron dapibus. Crunk tellus urna, pretizzle
daahng dawg, mattizzle izzle, eleifend vitae, nunc. Boofron suscipizzle.
Integizzle semper fo shizzle sizzle sizzle.</p>
<p>Phasellizzle mammasay mammasa mamma oo sa crackalackin tellizzle. Ut
phat ma nizzle that's the shizzle. Donizzle i saw beyonces tizzles and my
pizzle went crizzle cool. Nulla sapizzle phat, ultricizzle nizzle, accumsan
the bizzle, fermentizzle hizzle, pede. Sizzle nizzle ass. Etizzle owned
hizzle rizzle. Mauris uhuh ... yih!. Ma nizzle ut fo shizzle mah nizzle fo
rizzle, mah home g-dizzle varius nibh commodo commodo. Sure fo shizzle my
nizzle dolor brizzle fo, consectetizzle yo mamma elit. Sizzle ac mi. That's
the shizzle mi sizzle, sizzle izzle, away a, eleifend fo, for sure.</p>
</body>
</html>

45
src/.eslintrc Normal file
View File

@ -0,0 +1,45 @@
{
"extends": "airbnb",
"rules": {
"semi": [ 2, "never" ],
"no-confusing-arrow": 0,
"react/prop-types": 0,
"guard-for-in": 0,
"global-require": 0,
"no-underscore-dangle": 0,
"object-shorthand": 0,
"default-case": 0,
"one-var": 0,
"prefer-rest-params": 0,
"no-unused-vars": [ 2, { "args": "none" } ],
"no-alert": 0,
"quote-props": 0,
"no-nested-ternary": 0,
"no-use-before-define": [2, { "functions": false, "classes": true }],
"consistent-return": 0,
"no-eval": 0,
"prefer-arrow-callback": 0,
"array-bracket-spacing": 0,
"no-console": 0,
"indent": [ 2, 2, { "SwitchCase": 1 }],
"max-len": 0,
"comma-dangle": 0,
"no-param-reassign": 0,
"prefer-template": 0,
"curly": 0,
"func-names": 0,
"no-shadow": 0,
"spaced-comment": 0,
"strict": [ 2, "global" ]
},
"globals": {
"ENV": true,
"google": true,
"INITIAL_STATE": true,
"INITIAL_CONTEXT": true,
"PWD": true,
"$": true
}
}

20
src/app.js Normal file
View File

@ -0,0 +1,20 @@
import { $ } from 'dollr/dollr'
import Timer from './components/Timer'
// request permission on page load
document.addEventListener('DOMContentLoaded', () => {
if (!Notification) {
alert('Desktop notifications not available in your browser. Try Chromium.')
return
}
if (Notification.permission !== 'granted')
Notification.requestPermission()
})
$(() => {
const timer = $('.timer')
Timer.render().replace(timer).getWidget()
})

View File

@ -0,0 +1,87 @@
import markoWidgets from 'marko-widgets'
import template from './template.marko'
const length = 25 * 60 * 1000
export default markoWidgets.defineComponent({
template,
componentDidMount() {
this.reset()
},
start(e) {
this.setState({
startTime: Date.now()
})
this.interval = setInterval(this.updateTimer.bind(this), 10)
},
pause(e) {
clearInterval(this.interval)
},
reset(e) {
clearInterval(this.interval)
this.setState({
time: length,
totalTime: length,
startTime: undefined
})
},
updateTimer() {
let time = this.state.totalTime - (Date.now() - this.state.startTime)
if (time <= 0) {
this.pause()
this.end()
time = 0
}
this.setState({
time
})
},
end(time) {
const data = {
startTime: this.state.startTime,
endTime: new Date()
}
if (Notification.permission !== 'granted')
Notification.requestPermission()
else {
const notification = new Notification('Notification title', {
icon: 'http://cdn.sstatic.net/stackexchange/img/logos/so/so-icon.png',
body: 'Pomdoro is done!',
})
}
fetch('/api/pomodoros', {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then((response) => response.json())
.then((json) => {
console.log('succes')
})
},
getInitialState(input) {
return {
time: length,
totalTime: length,
startTime: undefined
}
}
})

View File

@ -0,0 +1,11 @@
<script marko-init>
const time = require('../../util/time-filter');
</script>
<div class="timer" w-bind>
<div class="time">${time(data.time || 0)}</div>
<div class="buttons">
<button w-onClick="start">Start</button>
<button>Pause</button>
<button>Reset</button>
</div>
</div>

3
src/error.marko Normal file
View File

@ -0,0 +1,3 @@
<section>
<h1>Error!</h1>
</section>

3
src/marko.json Normal file
View File

@ -0,0 +1,3 @@
{
"tags-dir": [ "./components" ]
}

31
src/master.marko Normal file
View File

@ -0,0 +1,31 @@
<!doctype html>
<html xml:lang="sv" lang="sv">
<head>
<title>Pomodoro</title>
<!-- Meta Tags -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="robots" content="index, follow" />
<meta name="description" content="Beta Pomodoro timer that stores your pomodors." />
<meta name="keywords" content="pomodoro,gtd,productivity" />
<meta name="author" content="Linus Miller" />
<!-- CSS -->
<link rel="stylesheet" href="/css/main${data.css && data.css.suffix}.css">
<!--[if IE]>
<link rel="stylesheet" type="text/css" href="css/default-IE.css" />
<![endif]-->
</head>
<body>
<main>
<h1>Pomodoro Time!</h1>
<Timer/>
</main>
<script src="/js/app${data.js && data.js.suffix}.js"></script>
</body>
</html>

45
src/routes.js Normal file
View File

@ -0,0 +1,45 @@
export default {
'': {
name: '',
//view: PageView,
template: require('./pages/index.marko'),
//subviews: {
// newsletterForm: [ '.newsletter', require('./views/newsletter-form') ]
//}
widget: require('./pages/index')
},
'samarbetspartners': {
//view: 'Page'
},
'om-oss': {
name: 'om-oss',
template: require('./pages/om-oss.marko'),
//view: PageView,
widget: require('./pages/om-oss')
},
'nyhetsbrev': {
name: 'nyhetsbrev',
template: require('./pages/nyhetsbrev.marko'),
//view: PageView,
widget: require('./pages/nyhetsbrev'),
//subviews: {
// newsletterForm: [ '.newsletter-form', require('./components/newsletter-form/view') ]
//},
},
'kontakt': {
name: 'kontakt',
//view: PageView,
template: require('./pages/kontakt.marko'),
widget: require('./pages/kontakt'),
//subviews: {
// googleMap: [ '#map-canvas', require('./views/google-map') ],
// contactForm: [ '.contact-form', require('./components/contact-form/view') ],
// newsletterForm: [ '.nyhetsbrev', require('./views/newsletter-form') ]
//}
}
};

19
src/util/split-time.js Normal file
View File

@ -0,0 +1,19 @@
'use strict';
const divs = [ 60, 100, 10 ];
module.exports = function (time) {
const arr = [];
for (let i = 0; i < divs.length; i++) {
const nbr = divs.slice(i).reduce((a, b) => a * b);
const result = Math.floor(time / nbr);
arr.push(result);
time = time - result * nbr;
//this.timerElements[i].textContent = result;
}
return arr;
};

10
src/util/time-filter.js Normal file
View File

@ -0,0 +1,10 @@
'use strict';
const splitTime = require('./split-time');
const twoDigits = require('./two-digits');
module.exports = function (time) {
const arr = splitTime(time).map(twoDigits);
return arr.join(':');
};

5
src/util/two-digits.js Normal file
View File

@ -0,0 +1,5 @@
'use strict';
module.exports = function twoDigits(val) {
return val >= 10 ? val : '0' + val;
};

2
static/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /