From 256842e42db99892ec4c43e76356abd5e9159aaa Mon Sep 17 00:00:00 2001 From: Linus Miller Date: Sun, 14 Aug 2016 13:27:48 +0200 Subject: [PATCH] Initial commit. --- .babelrc | 16 +++ .eslintrc | 52 ++++++++ .gitignore | 40 ++++++ README.md | 158 ++++++++++++++++++++++++ bin/create-roles.js | 47 +++++++ bin/create-user.js | 98 +++++++++++++++ doc/ideas.md | 1 + gulp/LICENSE | 22 ++++ gulp/README.md | 23 ++++ gulp/config.js | 121 ++++++++++++++++++ gulp/index.js | 30 +++++ gulp/less/functions.js | 42 +++++++ gulp/tasks/browser-sync.js | 9 ++ gulp/tasks/browserify.js | 126 +++++++++++++++++++ gulp/tasks/less.js | 49 ++++++++ gulp/tasks/nodemon.js | 35 ++++++ gulp/tasks/raster.js | 27 ++++ gulp/tasks/static.js | 39 ++++++ gulp/tasks/svg.js | 27 ++++ gulp/tasks/watch.js | 16 +++ gulp/tasks/wipe.js | 44 +++++++ gulp/util/error-handler.js | 16 +++ gulpfile.js | 7 ++ less/main.less | 0 package.json | 79 ++++++++++++ server/config/dir.js | 7 ++ server/config/error-handler.js | 58 +++++++++ server/config/globals.js | 4 + server/config/membership.js | 69 +++++++++++ server/config/mongo.js | 11 ++ server/config/port.js | 10 ++ server/config/session.js | 37 ++++++ server/config/site.js | 51 ++++++++ server/config/smtp.js | 15 +++ server/pages.js | 11 ++ server/server.js | 130 +++++++++++++++++++ server/services/pomodoros/middleware.js | 72 +++++++++++ server/services/pomodoros/model.js | 23 ++++ server/services/pomodoros/routes.js | 13 ++ server/templates/error.marko | 15 +++ server/templates/index.marko | 32 +++++ src/.eslintrc | 45 +++++++ src/app.js | 20 +++ src/components/Timer/index.js | 87 +++++++++++++ src/components/Timer/template.marko | 11 ++ src/error.marko | 3 + src/marko.json | 3 + src/master.marko | 31 +++++ src/routes.js | 45 +++++++ src/util/split-time.js | 19 +++ src/util/time-filter.js | 10 ++ src/util/two-digits.js | 5 + static/robots.txt | 2 + 53 files changed, 1963 insertions(+) create mode 100644 .babelrc create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bin/create-roles.js create mode 100755 bin/create-user.js create mode 100644 doc/ideas.md create mode 100644 gulp/LICENSE create mode 100644 gulp/README.md create mode 100644 gulp/config.js create mode 100644 gulp/index.js create mode 100644 gulp/less/functions.js create mode 100644 gulp/tasks/browser-sync.js create mode 100644 gulp/tasks/browserify.js create mode 100644 gulp/tasks/less.js create mode 100644 gulp/tasks/nodemon.js create mode 100644 gulp/tasks/raster.js create mode 100644 gulp/tasks/static.js create mode 100644 gulp/tasks/svg.js create mode 100644 gulp/tasks/watch.js create mode 100644 gulp/tasks/wipe.js create mode 100644 gulp/util/error-handler.js create mode 100644 gulpfile.js create mode 100644 less/main.less create mode 100644 package.json create mode 100644 server/config/dir.js create mode 100644 server/config/error-handler.js create mode 100644 server/config/globals.js create mode 100644 server/config/membership.js create mode 100644 server/config/mongo.js create mode 100644 server/config/port.js create mode 100644 server/config/session.js create mode 100644 server/config/site.js create mode 100644 server/config/smtp.js create mode 100644 server/pages.js create mode 100644 server/server.js create mode 100644 server/services/pomodoros/middleware.js create mode 100644 server/services/pomodoros/model.js create mode 100644 server/services/pomodoros/routes.js create mode 100644 server/templates/error.marko create mode 100644 server/templates/index.marko create mode 100644 src/.eslintrc create mode 100644 src/app.js create mode 100644 src/components/Timer/index.js create mode 100644 src/components/Timer/template.marko create mode 100644 src/error.marko create mode 100644 src/marko.json create mode 100644 src/master.marko create mode 100644 src/routes.js create mode 100644 src/util/split-time.js create mode 100644 src/util/time-filter.js create mode 100644 src/util/two-digits.js create mode 100644 static/robots.txt diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..b7ba315 --- /dev/null +++ b/.babelrc @@ -0,0 +1,16 @@ +{ + "env": { + "server": { + "presets": [ "es2015-node6" ], + "plugins": [ + "add-module-exports" + ] + }, + "client": { + "presets": [ "es2015", "react" ], + "plugins": [ + "add-module-exports" + ] + } + } +} diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..ca97434 --- /dev/null +++ b/.eslintrc @@ -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 + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76772cd --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6789b23 --- /dev/null +++ b/README.md @@ -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. diff --git a/bin/create-roles.js b/bin/create-roles.js new file mode 100755 index 0000000..a5e2533 --- /dev/null +++ b/bin/create-roles.js @@ -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(); diff --git a/bin/create-user.js b/bin/create-user.js new file mode 100755 index 0000000..09a4523 --- /dev/null +++ b/bin/create-user.js @@ -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)); diff --git a/doc/ideas.md b/doc/ideas.md new file mode 100644 index 0000000..ea24472 --- /dev/null +++ b/doc/ideas.md @@ -0,0 +1 @@ ++ timer in document title (so you can see it in the browser tab) diff --git a/gulp/LICENSE b/gulp/LICENSE new file mode 100644 index 0000000..27e2991 --- /dev/null +++ b/gulp/LICENSE @@ -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. + diff --git a/gulp/README.md b/gulp/README.md new file mode 100644 index 0000000..f1aefe2 --- /dev/null +++ b/gulp/README.md @@ -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 diff --git a/gulp/config.js b/gulp/config.js new file mode 100644 index 0000000..a742a40 --- /dev/null +++ b/gulp/config.js @@ -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') ] + } +} diff --git a/gulp/index.js b/gulp/index.js new file mode 100644 index 0000000..afb3c28 --- /dev/null +++ b/gulp/index.js @@ -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)) diff --git a/gulp/less/functions.js b/gulp/less/functions.js new file mode 100644 index 0000000..5229d78 --- /dev/null +++ b/gulp/less/functions.js @@ -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') + } +} diff --git a/gulp/tasks/browser-sync.js b/gulp/tasks/browser-sync.js new file mode 100644 index 0000000..2e5b47e --- /dev/null +++ b/gulp/tasks/browser-sync.js @@ -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)) diff --git a/gulp/tasks/browserify.js b/gulp/tasks/browserify.js new file mode 100644 index 0000000..7d5327d --- /dev/null +++ b/gulp/tasks/browserify.js @@ -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) +}) diff --git a/gulp/tasks/less.js b/gulp/tasks/less.js new file mode 100644 index 0000000..11f3b52 --- /dev/null +++ b/gulp/tasks/less.js @@ -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)) +}) diff --git a/gulp/tasks/nodemon.js b/gulp/tasks/nodemon.js new file mode 100644 index 0000000..b15a9fa --- /dev/null +++ b/gulp/tasks/nodemon.js @@ -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() + } + }) + }) +}) diff --git a/gulp/tasks/raster.js b/gulp/tasks/raster.js new file mode 100644 index 0000000..b1978a4 --- /dev/null +++ b/gulp/tasks/raster.js @@ -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') + }) +}) diff --git a/gulp/tasks/static.js b/gulp/tasks/static.js new file mode 100644 index 0000000..13928ec --- /dev/null +++ b/gulp/tasks/static.js @@ -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') + }) +}) diff --git a/gulp/tasks/svg.js b/gulp/tasks/svg.js new file mode 100644 index 0000000..97679ad --- /dev/null +++ b/gulp/tasks/svg.js @@ -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') + }) +}) diff --git a/gulp/tasks/watch.js b/gulp/tasks/watch.js new file mode 100644 index 0000000..a36e5a5 --- /dev/null +++ b/gulp/tasks/watch.js @@ -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)) + }) +}) diff --git a/gulp/tasks/wipe.js b/gulp/tasks/wipe.js new file mode 100644 index 0000000..c082a42 --- /dev/null +++ b/gulp/tasks/wipe.js @@ -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() + } + } + }) + }) +}) diff --git a/gulp/util/error-handler.js b/gulp/util/error-handler.js new file mode 100644 index 0000000..8ac528a --- /dev/null +++ b/gulp/util/error-handler.js @@ -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') + } +} diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..5882dc4 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,7 @@ +'use strict'; + +require('./gulp'); + +process.on('SIGINT', () => { + process.exit(0); +}); diff --git a/less/main.less b/less/main.less new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json new file mode 100644 index 0000000..5161ba8 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/server/config/dir.js b/server/config/dir.js new file mode 100644 index 0000000..20c1347 --- /dev/null +++ b/server/config/dir.js @@ -0,0 +1,7 @@ +'use strict' + +const p = require('path') + +module.exports = { + static: p.join(PWD, 'public'), +} diff --git a/server/config/error-handler.js b/server/config/error-handler.js new file mode 100644 index 0000000..43cab4a --- /dev/null +++ b/server/config/error-handler.js @@ -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]) diff --git a/server/config/globals.js b/server/config/globals.js new file mode 100644 index 0000000..c6c45c0 --- /dev/null +++ b/server/config/globals.js @@ -0,0 +1,4 @@ +'use strict' + +global.LOGIN_USER = 'lohfu@lohfu.io' +//global.LOGIN_USER = 'zarac@zarac.se' diff --git a/server/config/membership.js b/server/config/membership.js new file mode 100644 index 0000000..4076e75 --- /dev/null +++ b/server/config/membership.js @@ -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 + // } + //} + } +} diff --git a/server/config/mongo.js b/server/config/mongo.js new file mode 100644 index 0000000..a82ae5c --- /dev/null +++ b/server/config/mongo.js @@ -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]) diff --git a/server/config/port.js b/server/config/port.js new file mode 100644 index 0000000..dcb3848 --- /dev/null +++ b/server/config/port.js @@ -0,0 +1,10 @@ +'use strict' + +const basePort = 3050 + +module.exports = { + development: basePort, + testing: basePort + 1, + staging: basePort + 2, + production: basePort + 3 +}[ENV] diff --git a/server/config/session.js b/server/config/session.js new file mode 100644 index 0000000..cb50cc2 --- /dev/null +++ b/server/config/session.js @@ -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 + diff --git a/server/config/site.js b/server/config/site.js new file mode 100644 index 0000000..83ac59d --- /dev/null +++ b/server/config/site.js @@ -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]) diff --git a/server/config/smtp.js b/server/config/smtp.js new file mode 100644 index 0000000..44763e1 --- /dev/null +++ b/server/config/smtp.js @@ -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]) diff --git a/server/pages.js b/server/pages.js new file mode 100644 index 0000000..90aad6c --- /dev/null +++ b/server/pages.js @@ -0,0 +1,11 @@ +'use strict' + +const masterTemplate = require('../src/master.marko') + +module.exports = [ + [ '/', 'get', (req, res, next) => { + res.template = masterTemplate + + next() + } ], +] diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..50805df --- /dev/null +++ b/server/server.js @@ -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 diff --git a/server/services/pomodoros/middleware.js b/server/services/pomodoros/middleware.js new file mode 100644 index 0000000..a96bc43 --- /dev/null +++ b/server/services/pomodoros/middleware.js @@ -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 +} + diff --git a/server/services/pomodoros/model.js b/server/services/pomodoros/model.js new file mode 100644 index 0000000..ad992f4 --- /dev/null +++ b/server/services/pomodoros/model.js @@ -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) diff --git a/server/services/pomodoros/routes.js b/server/services/pomodoros/routes.js new file mode 100644 index 0000000..a8cf64c --- /dev/null +++ b/server/services/pomodoros/routes.js @@ -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 ]] +] diff --git a/server/templates/error.marko b/server/templates/error.marko new file mode 100644 index 0000000..70f2384 --- /dev/null +++ b/server/templates/error.marko @@ -0,0 +1,15 @@ + + + + Newseri - ERROR + + + + + +
+

Error ${data.error.status}

+
${JSON.stringify(data.error, null, '  ')}
+
+ + diff --git a/server/templates/index.marko b/server/templates/index.marko new file mode 100644 index 0000000..55a0374 --- /dev/null +++ b/server/templates/index.marko @@ -0,0 +1,32 @@ + + + + Budbilar + + + + + +

Välkommen till Budbilar

+ +

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.

+ +

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.

+ + diff --git a/src/.eslintrc b/src/.eslintrc new file mode 100644 index 0000000..9cbe673 --- /dev/null +++ b/src/.eslintrc @@ -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 + } +} diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..671966d --- /dev/null +++ b/src/app.js @@ -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() +}) diff --git a/src/components/Timer/index.js b/src/components/Timer/index.js new file mode 100644 index 0000000..d6f61f9 --- /dev/null +++ b/src/components/Timer/index.js @@ -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 + } + } +}) diff --git a/src/components/Timer/template.marko b/src/components/Timer/template.marko new file mode 100644 index 0000000..b4e394a --- /dev/null +++ b/src/components/Timer/template.marko @@ -0,0 +1,11 @@ + +
+
${time(data.time || 0)}
+
+ + + +
+
diff --git a/src/error.marko b/src/error.marko new file mode 100644 index 0000000..e183c9c --- /dev/null +++ b/src/error.marko @@ -0,0 +1,3 @@ +
+

Error!

+
diff --git a/src/marko.json b/src/marko.json new file mode 100644 index 0000000..30247b3 --- /dev/null +++ b/src/marko.json @@ -0,0 +1,3 @@ +{ + "tags-dir": [ "./components" ] +} diff --git a/src/master.marko b/src/master.marko new file mode 100644 index 0000000..c0c443d --- /dev/null +++ b/src/master.marko @@ -0,0 +1,31 @@ + + + + Pomodoro + + + + + + + + + + + + + + + + + +
+

Pomodoro Time!

+ +
+ + + + diff --git a/src/routes.js b/src/routes.js new file mode 100644 index 0000000..8144921 --- /dev/null +++ b/src/routes.js @@ -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') ] + //} + } +}; diff --git a/src/util/split-time.js b/src/util/split-time.js new file mode 100644 index 0000000..0249b5a --- /dev/null +++ b/src/util/split-time.js @@ -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; +}; diff --git a/src/util/time-filter.js b/src/util/time-filter.js new file mode 100644 index 0000000..94b89f1 --- /dev/null +++ b/src/util/time-filter.js @@ -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(':'); +}; diff --git a/src/util/two-digits.js b/src/util/two-digits.js new file mode 100644 index 0000000..b7cf451 --- /dev/null +++ b/src/util/two-digits.js @@ -0,0 +1,5 @@ +'use strict'; + +module.exports = function twoDigits(val) { + return val >= 10 ? val : '0' + val; +}; diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /