diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..66b2078 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,20 @@ +--- +engines: + duplication: + enabled: true + config: + languages: + javascript: + mass_threshold: 60 + eslint: + enabled: true + fixme: + enabled: true + scss-lint: + enabled: true +ratings: + paths: + - "**.js" + - "**.scss" +exclude_paths: +- bin/ \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..5b3b359 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/*{.,-}min.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..a5b1e18 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,78 @@ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 8, + sourceType: 'module' + }, + extends: 'eslint:recommended', + env: { + browser: true, + es6: true, + jquery: true, + commonjs: true, + node: true + }, + globals: { + '-Promise': true, + 'require': true, + 'module': true + }, + rules: { + 'semi': ['error', 'always'], + 'no-unused-vars': ['error', { 'args': 'after-used' }], + 'curly': 'error', + 'no-alert': 'error', + 'eqeqeq': 'error', + 'no-eval': 'error', + 'no-implied-eval': 'error', + 'guard-for-in': 'error', + 'wrap-iife': 'error', + 'no-caller': 'error', + 'no-new': 'error', + 'no-eq-null': 'error', + 'no-unused-expressions': 'error', + 'accessor-pairs': 'error', + 'block-scoped-var': 'error', + 'no-loop-func': 'error', + 'dot-notation': 'error', + 'no-div-regex': 'error', + 'no-extra-bind': 'error', + 'no-iterator': 'error', + 'no-labels': 'error', + 'no-lone-blocks': 'error', + 'no-new-func': 'error', + 'no-new-wrappers': 'error', + 'no-octal-escape': 'error', + 'no-proto': 'error', + 'no-return-assign': 'error', + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-useless-call': 'error', + 'no-useless-concat': 'error', + 'no-void': 'error', + 'no-with': 'error', + 'radix': 'error', + 'yoda': 'error', + 'no-catch-shadow': 'error', + 'no-label-var': 'error', + 'no-shadow-restricted-names': 'error', + 'no-undef-init': 'error', + 'no-use-before-define': 'error', + 'callback-return': 'error', + 'global-require': 'error', + 'handle-callback-err': 'error', + 'no-path-concat': 'error', + 'no-process-exit': 'error', + 'block-spacing': 'error', + 'brace-style': ['error', '1tbs', { allowSingleLine: true }], + 'comma-spacing': ['error', { before: false, after: true }], + 'consistent-this': 'error', + 'indent': ['error', 4, { 'SwitchCase': 1 }], + 'object-curly-spacing': ['error', 'always', { objectsInObjects: false }], + 'no-var': 'error', + 'require-yield': 'error', + 'space-before-function-paren': ['error', { anonymous: 'always', named: 'never', asyncArrow: 'always' }], + 'object-shorthand': ['error', 'always', { avoidExplicitReturnArrows: true }], + 'no-console': ["warn", { allow: ["warn", "error"] }] + } +}; \ No newline at end of file diff --git a/.gitignore b/.gitignore index ecc8824..4c52665 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,40 @@ -.sass-cache -.vagrant +# 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 + +# Dependency directory +# https://docs.npmjs.com/cli/shrinkwrap#caveats node_modules -npm-debug.log \ No newline at end of file +bower_components + +# Debug log from npm +npm-debug.log + +# Sass +.sass-cache +*.map + +# misc +.DS_STORE +jsconfig.json +.vscode diff --git a/.pug-lintrc b/.pug-lintrc new file mode 100644 index 0000000..20092b0 --- /dev/null +++ b/.pug-lintrc @@ -0,0 +1,31 @@ +{ + "disallowAttributeConcatenation": true, + "disallowAttributeInterpolation": true, + "disallowBlockExpansion": true, + "disallowClassAttributeWithStaticValue": true, + "disallowClassLiteralsBeforeIdLiterals": true, + "disallowDuplicateAttributes": true, + "disallowHtmlText": true, + "disallowIdAttributeWithStaticValue": true, + "disallowLegacyMixinCall": true, + "disallowMultipleLineBreaks": true, + "disallowSpaceAfterCodeOperator": true, + "disallowSpacesInsideAttributeBrackets": true, + "requireClassLiteralsBeforeAttributes": true, + "requireIdLiteralsBeforeAttributes": true, + "requireLowerCaseAttributes": true, + "requireLowerCaseTags": true, + "requireSpecificAttributes": [ + { + "img": [ + "alt" + ] + } + ], + "requireStrictEqualityOperators": true, + "validateAttributeSeparator": " ", + "validateDivTags": true, + "validateIndentation": 4, + "validateSelfClosingTags": true, + "validateTemplateString": true +} \ No newline at end of file diff --git a/.sass-lint.yml b/.sass-lint.yml new file mode 100644 index 0000000..5069b0b --- /dev/null +++ b/.sass-lint.yml @@ -0,0 +1,96 @@ +files: + include: '**/*.s+(a|c)ss' +options: + formatter: stylish + merge-default-rules: true +rules: + extends-before-mixins: 0 + extends-before-declarations: 0 + placeholder-in-extend: 0 + + # Mixins + mixins-before-declarations: 0 + + # Line Spacing + one-declaration-per-line: 2 + empty-line-between-blocks: 2 + single-line-per-selector: 2 + + # Disallows + no-attribute-selectors: 0 + no-color-hex: 0 + no-color-keywords: 2 + no-color-literals: + - 2 + - allow-rgba: true + no-combinators: 0 + no-css-comments: 2 + no-debug: 2 + no-disallowed-properties: 0 + no-duplicate-properties: 2 + no-empty-rulesets: 2 + no-extends: 0 + no-ids: 2 + no-important: 2 + no-invalid-hex: 2 + no-mergeable-selectors: 2 + no-misspelled-properties: 2 + no-qualifying-elements: 0 + no-trailing-whitespace: 2 + no-trailing-zero: 2 + no-transition-all: 2 + no-universal-selectors: 0 + no-url-domains: 2 + no-url-protocols: 2 + no-vendor-prefixes: 2 + no-warn: 2 + property-units: 0 + + # Nesting + declarations-before-nesting: 2 + force-attribute-nesting: 2 + force-element-nesting: 0 + force-pseudo-nesting: 0 + + # Name Formats + class-name-format: 2 + function-name-format: 2 + id-name-format: 0 + + # Style Guide + attribute-quotes: 2 + bem-depth: 0 + border-zero: 2 + brace-style: 2 + clean-import-paths: 2 + empty-args: 0 + hex-length: 2 + hex-notation: 2 + indentation: + - 2 + - size: 4 + leading-zero: 2 + max-line-length: 0 + max-file-line-count: 0 + nesting-depth: 0 + property-sort-order: 0 + pseudo-element: 2 + quotes: 2 + shorthand-values: 2 + url-quotes: 2 + variable-for-property: 2 + zero-unit: 2 + + # Inner Spacing + space-after-comma: 2 + space-before-colon: 2 + space-after-colon: 2 + space-before-brace: 2 + space-before-bang: 2 + space-after-bang: 2 + space-between-parens: 2 + space-around-operator: 2 + + # Final Items + trailing-semicolon: 2 + final-newline: 0 diff --git a/.travis.yml b/.travis.yml index 6770908..958317a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,8 @@ language: node_js node_js: - - "5" - - "5.1" - - "4" - - "4.2" - - "4.1" - - "4.0" - - "0.12" - - "0.11" - - "0.10" -before_install: + - "7" + - "6" +before_install: - npm install -g grunt-cli - - gem install sass -install: npm install -before_script: grunt \ No newline at end of file +install: + - npm install \ No newline at end of file diff --git a/LICENSE b/LICENSE index 387f7e7..1596ddb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -The MIT License (MIT) +MIT License Copyright (c) 2016 Stefan Cosma @@ -18,4 +18,4 @@ 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. +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 753847a..bc68a9d 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,56 @@ -uptimey -======= +# uptimey +> Simple server uptime monitor -[![Build Status](https://travis-ci.org/stefanbc/uptimey.svg?branch=master)](https://travis-ci.org/stefanbc/uptimey) [![Dependency Status](https://www.versioneye.com/user/projects/572c7efaa0ca35004cf77288/badge.svg?style=flat)](https://www.versioneye.com/user/projects/572c7efaa0ca35004cf77288) [![Built with Grunt](https://cdn.gruntjs.com/builtwith.svg)](http://gruntjs.com/) +[![Build Status](https://travis-ci.org/stefanbc/uptimey.svg?branch=master)](https://travis-ci.org/stefanbc/uptimey) [![Dependency Status](https://dependencyci.com/github/stefanbc/uptimey/badge)](https://dependencyci.com/github/stefanbc/uptimey) [![Code Climate](https://codeclimate.com/github/stefanbc/uptimey/badges/gpa.svg)](https://codeclimate.com/github/stefanbc/uptimey) +With uptimey you can easily monitor your server's uptime. It will output usefull data that you might need during your day, while you intereact with your server. Don't believe me, check out the screenshot bellow. -If you're proud of your server uptime, because you put a lot of time into configuring it, then you can showcase it with **uptimey** - a beautiful Server Uptime Monitor! +**Import note:** it works on systems that run macOS, Linux and Windows. -Just clone the repo on your web server and then access your server's host followed by `/uptimey`, in your browser. Simple as that! +![Screenshot](http://i.imgur.com/bxBd87M.png) -Features --- +## Prerequisites -* The background image is a random image from [Unsplash](https://unsplash.com)! -* Works on Linux, Windows, Mac OS servers. -* Automatically gathers data from the server. -* Knows if it's nighttime or daytime. -* Knows the aprox server location (based on IP). -* Tweet your awesome uptime! -* Screenshot the server uptime and show it to your devops buddies! :) -* Configure it to your liking. You can modify the `client/bin/settings.json` file. Checkout the [SETTINGS.md](SETTINGS.md) file for more info on how to change the parameters. +You will need the following things properly installed on your computer. -![Screenshot](https://i.imgur.com/sbvuMBB.png) +* [Git](http://git-scm.com/) +* [Node.js](http://nodejs.org/) (with NPM) +* [Bower](http://bower.io/) -Requirements --- +## Installation -* Apache server -* PHP -* Access to the Internet +* `git clone git@github.com:stefanbc/uptimey.git` this repository +* `cd uptimey` +* `npm install` +* `bower install` -Developers --- +Alternatively you can use the sh script inside the repo, after you've cloned it. Make sure it has execution permissions. -Make sure you have Node and npm installed. You'll need to have Grunt and Sass installed. Use these commands: +Eg. `./uptimey.sh init` or `./uptimey.sh help` -``` -npm install -g grunt-cli -gem install sass -``` +## Running -You can then install all the project dependencies using: +* `npm start` +* Open [http://localhost:3000](http://localhost:3000) and behold uptimey in all it's simple glory. -``` -npm install -``` +## Development -Available Grunt tasks: +You'll need to have Grunt and Sass installed. Use these commands: -* `grunt` - will build the whole project. -* `grunt watch` - will watch for any file modifications and will build. Will also build on start. -* `grunt test` - will test the main app js file using `jshint` (more tests are coming soon). +* `npm install -g grunt-cli` +* `gem install sass` -For local development you can use Vagrant and you can check if the build passes using Travis-CI. +### Running Tests + +* `grunt test` + +### Building + +* `grunt dev` (development) +* `grunt` (production) + +## Meta + +Stefan Cosma – [@stefanbc](https://twitter.com/stefanbc) – uptimey@stefancosma.xyz + +Distributed under the MIT license. See ``LICENSE`` for more information. \ No newline at end of file diff --git a/SETTINGS.md b/SETTINGS.md deleted file mode 100644 index ce8fc91..0000000 --- a/SETTINGS.md +++ /dev/null @@ -1,173 +0,0 @@ -Settings -=== - -If you want to modify the way uptimey behaves and looks you can use the `client/bin/settings.json` file. This file once modified will adjust various things. - -Below are the things that can be modified and the explanation to each one of them. - -File structure ---- - -By default this is how the file should look. By removing any of the lines, you might break the app. - -``` -{ - "background_color" : "", - "background_image" : "", - "buttons": [{ - "refresh" : true, - "advanced" : true, - "twitter" : true, - "google-plus" : false, - "facebook" : false, - "screenshot" : true - }], - "debug_mode" : false, - "default_view" : "default", - "display_timezone" : "Europe/Bucharest", - "font_color" : "", - "font_family" : "", - "menu_placement" : "top", - "remove_menu" : false, - "show_am_pm" : true, - "show_location" : true, - "show_menu_always" : false, - "use_24h_clock" : false -} -``` - -Parameters ---- - -``` -background_color -``` -**(string) (optional)** You can set the desired background color for the app in a HEX or RGB format. - -**Default**: empty - ---- - -``` -background_image -``` -**(string) (optional)** By default the app uses a random image from [Unsplash](http://unsplash.com) but you can specify an URL to another image. - -**Default**: empty - ---- - -``` -buttons -``` -**(bool) (required)** If you want you can disable or enable one of the buttons in the top menu using a boolean value. Available buttons are: refresh, advanced, twitter, google-plus, facebook, screenshot. - -**Default**: - -``` -"refresh" : true, -"advanced" : true, -"twitter" : true, -"google-plus" : false, -"facebook" : false, -"screenshot" : true -``` - ---- - -``` -debug_mode -``` -**(bool) (optional)** You can enable the debug mode for the app. - -**Default**: false - ---- - -``` -default_view -``` -**(string) (required)** Changed the default view of the app. Available options include: default, advanced. - -**Default**: default - ---- - -``` -display_timezone -``` -**(string) (required)** Set the display timezone for all dates and time featured in the app. You can use the timezones featured [here](http://php.net/manual/en/timezones.php). - -**Default**: Europe/Bucharest - ---- - -``` -font_color -``` -**(string) (optional)** You can set the desired font color for the app in a HEX or RGB format. - -**Default**: empty - ---- - -``` -font_family -``` -**(string) (optional)** You can set the desired font family for the app. - -**Default**: empty - ---- - -``` -menu_placement -``` -**(string) (required)** Set the default placement for the top menu. Available options include: top, bottom, left, right. - -**Default**: top - ---- - -``` -remove_menu -``` -**(bool) (optional)** Remove the main buttons menu entirely. - -**Default**: false - ---- - -``` -show_am_pm -``` -**(bool) (required)** Use this to show or hide the AM / PM operators when showing the time. - -**Default**: true - ---- - -``` -show_location -``` -**(bool) (required)** If you want to show or hide the location of your server you can use this parameter. - -**Default**: true - ---- - -``` -show_menu_always -``` -**(bool) (optional)** Use this parameter if you want to main button menu to be always toggled and opened. - -**Default**: false - ---- - -``` -use_24h_clock -``` -**(bool) (optional)** You can show the clock in a 24h format if you don't want to show it in a 12h format. - -**Default**: false diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index df77c06..0000000 --- a/Vagrantfile +++ /dev/null @@ -1,11 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! -VAGRANTFILE_API_VERSION = "2" - -Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| - config.vm.box = "hashicorp/precise32" - config.vm.provision :shell, path: "vagrant-setup.sh" - config.vm.network "forwarded_port", guest: 80, host: 8080 -end \ No newline at end of file diff --git a/app/app.js b/app/app.js new file mode 100644 index 0000000..db78e5c --- /dev/null +++ b/app/app.js @@ -0,0 +1,7 @@ +const $ = require('jquery'); +const index = require('./controllers/index'); + +// The only thing that should be in a DOMReady +$(function () { + index.init(); +}); \ No newline at end of file diff --git a/app/controllers/index.js b/app/controllers/index.js new file mode 100644 index 0000000..4b8701b --- /dev/null +++ b/app/controllers/index.js @@ -0,0 +1,41 @@ +const $ = require('jquery'); +const api = require('../helpers/api'); +const utils = require('../helpers/utils'); +const actions = require('../helpers/actions'); + +/** + * Controller for the index route + */ +module.exports = { + /** + * Init method + */ + init() { + if ( utils.getCurrentLayout() === 'index' ) { + + api.get({ + route : 'basic', + updates : true, + callback(data) { + let days = `${data.uptime.days} days`, + hours = `${data.uptime.hours} hours`, + minutes = `${data.uptime.minutes} minutes`; + + $('title').text(`uptimey - ${days} ${hours} ${minutes}`); + } + }); + + api.get({ + route : 'advanced', + updates : false, + callback() { + actions.register({ + ev: 'copy', + selector: '.list-value' + }); + } + }); + + } + } +}; \ No newline at end of file diff --git a/app/helpers/actions.js b/app/helpers/actions.js new file mode 100644 index 0000000..c93a99a --- /dev/null +++ b/app/helpers/actions.js @@ -0,0 +1,55 @@ +const _ = require('lodash'); +const $ = require('jquery'); +const toasts = require('./toasts'); +const common = require('./common'); + +/** + * Register actions + */ +module.exports = { + /** + * Register an action + * @param {Object} options + */ + register(options) { + _.bindAll(this); + + this[options.ev](options.selector); + }, + + /** + * Copy action + * @param {String} selector + */ + copy(selector) { + let actionIcon = $(selector).find('.copy-action'); + + common.insertIcon(selector, 'copy', 'clippy'); + + actionIcon.on('click', (ev) => { + let element = $(ev.currentTarget).parent().find('.output'); + + this.copyToClipboard(element[0]); + }); + }, + + /** + * Copies an elements text to clipboard + * @param {Object} element + */ + copyToClipboard(element) { + let text = element, + selection = window.getSelection(), + range = document.createRange(); + + range.selectNodeContents(text); + selection.removeAllRanges(); + selection.addRange(range); + + document.execCommand('copy'); + + toasts.init('success', 'Value copied to clipboard'); + + selection.removeAllRanges(); + }, +}; \ No newline at end of file diff --git a/app/helpers/api.js b/app/helpers/api.js new file mode 100644 index 0000000..77eb9d9 --- /dev/null +++ b/app/helpers/api.js @@ -0,0 +1,156 @@ +const _ = require('lodash'); +const $ = require('jquery'); +const moment = require('moment'); +const utils = require('./utils'); +const notes = require('./notes'); +const toasts = require('./toasts'); + +/** + * Helper for API interaction + */ +module.exports = { + + updateTimeout: 1000 * 60, + + /** + * Makes an API call using the provided params + * + * Available options are: + * {String} url + * {Boolean} updates + * {Function} callback + * + * @param {Object} options + */ + get(options) { + + _.bindAll(this); + + if (options.updates) { + + setInterval(() => { + this._ajax(options); + }, this.updateTimeout); + + } else { + this._ajax(options); + } + + }, + + /** + * Makes an Ajax call with the passed options + * @param {Object} options + */ + _ajax(options) { + + let normalizeUrl = this.buildUrl(options.route); + + $.ajax({ + dataType: "json", + url: normalizeUrl, + success: _.bind((data) => { + + this.bindData(data, options.updates); + + if (options.updates) { + notes.clearAll(); + toasts.init('success', 'Data has been updated!'); + } + + $('ul.list-values').removeClass('loading'); + + if (options.callback) { + return options.callback(data); + } + + }, this), + error: _.bind(() => { + + this.requestTimer(); + this.bindDataNotes(); + + toasts.init('error', 'Server is not responding!'); + + }, this) + }); + + }, + + /** + * Binds data to DOM elements + * @param {Object} data + * @param {Boolean} updates + */ + bindData(data, updates) { + // Recursive function to update values + function updateValues(key, value) { + if (typeof value !== 'object') { + let selector = '#' + utils.normalizeString(key); + + if ($(selector).find('span').length === 1) { + $(selector).find('span').text(value); + } else { + $(selector).text(value); + } + + if (updates) { + $(selector).data('data-updates', true); + } + + } else { + $.each(value, _.bind(updateValues, this)); + } + } + + $.each(data, _.bind(updateValues, this)); + }, + + /** + * Binds notes to all data + */ + bindDataNotes() { + let outputBoxes = $('.data-wrapper .output'); + + $.each(outputBoxes, function () { + let updates = $(this).data('data-updates'), + key = $(this).attr('id'), + selector = $(`#${key}`).parents('.box'); + + if (updates) { + notes.init('error', 'Failed to update data!', selector); + } + }); + }, + + /** + * Builds an url for API calls + * @param {String} string + */ + buildUrl(string) { + return `/api${string ? '/' + string : ''}`; + }, + + /** + * Request timer when connection to server is down + */ + requestTimer() { + let interval = 1000, + duration = moment.duration(this.updateTimeout * 1000, 'milliseconds'), + counter = setInterval(() => { + + duration = moment.duration(duration.asMilliseconds() - interval, 'milliseconds'); + + let output = moment(duration.asMilliseconds()).format('ss'); + + $('.request-timer-wrapper').removeClass('hide'); + $('.request-timer').text(output); + + if (output === '00') { + clearInterval(counter); + $('.request-timer-wrapper').addClass('hide'); + } + + }, interval); + } +}; \ No newline at end of file diff --git a/app/helpers/common.js b/app/helpers/common.js new file mode 100644 index 0000000..3622dc6 --- /dev/null +++ b/app/helpers/common.js @@ -0,0 +1,37 @@ +const $ = require('jquery'); +const octicons = require("octicons"); + +/** + * Common helpers + */ +module.exports = { + /** + * Inserts an icon within the desired element + * @param {Object} selector + * @param {String} action + * @param {String} icon + */ + insertIcon(selector, action, icon) { + let actionIcon = $(selector).find(`.${action}-action`); + + actionIcon.addClass('tooltip tooltip-right').attr('data-tooltip', action); + + $(selector).on('mouseenter mouseleave', (ev) => { + let type = ev.type; + + if (type === 'mouseenter') { + actionIcon.append(this.generateIcon(icon)); + } else if (type === 'mouseleave') { + actionIcon.find('.icon').remove(); + } + }); + }, + + /** + * Outputs the correct markup for the oction + * @param {String} icon + */ + generateIcon(icon) { + return `${octicons[icon].toSVG()}`; + } +}; \ No newline at end of file diff --git a/app/helpers/notes.js b/app/helpers/notes.js new file mode 100644 index 0000000..10d8f3b --- /dev/null +++ b/app/helpers/notes.js @@ -0,0 +1,79 @@ +const _ = require('lodash'); +const $ = require('jquery'); + +/** + * Helper for generating notes + */ +module.exports = { + + notesDefaultParent: '.box', + defaultPosition: 'tr', + + /** + * The main method for generating a note + * @param {String} type + * @param {String} msg + * @param {Object} parent + * @param {String} position + */ + init(type, msg, parent = this.notesDefaultParent, position = this.defaultPosition) { + _.bindAll(this); + + this[type](msg, parent, position); + }, + + /** + * Generate a success note + * @param {String} msg + * @param {Object} parent + * @param {String} position + */ + success(msg, parent, position) { + let note = this.buildNote('success', msg, position); + + if (!this.checkForNote(parent)) { + parent.append(note); + } + }, + + /** + * Generate an error note + * @param {String} msg + * @param {Object} parent + * @param {String} position + */ + error(msg, parent, position) { + let note = this.buildNote('danger', msg, position); + + if (!this.checkForNote(parent)) { + parent.append(note); + } + }, + + /** + * Build a note and animate it + * @param {String} type + * @param {String} msg + * @param {String} position + */ + buildNote(type, msg, position) { + let note = `
`; + + return note; + }, + + /** + * Checks for the existance of a note + * @param {String} selector + */ + checkForNote(selector) { + return (selector.find('.note').length !== 0) ? true : false; + }, + + /** + * Clears all notes + */ + clearAll() { + $(this.notesDefaultParent).find('.note').remove(); + } +}; \ No newline at end of file diff --git a/app/helpers/toasts.js b/app/helpers/toasts.js new file mode 100644 index 0000000..022c6fb --- /dev/null +++ b/app/helpers/toasts.js @@ -0,0 +1,76 @@ +const _ = require('lodash'); +const $ = require('jquery'); + +/** + * Helper for generating toasts + */ +module.exports = { + + wormHole: '.container .toasts-wormhole', + defaultPosition: 'tr', + + /** + * The main method for generating a toast + * @param {String} type + * @param {String} msg + * @param {String} position + */ + init(type, msg, position = this.defaultPosition) { + _.bindAll(this); + + this.clearAll(); + this[type](msg, position); + + _.delay(() => { + this.hide(); + }, 6000); + }, + + /** + * Generate a success toast + * @param {String} msg + * @param {String} position + */ + success(msg, position) { + let toast = this.buildToast('success', msg, position); + + $(this.wormHole).append(toast); + }, + + /** + * Generate an error toast + * @param {String} msg + * @param {String} position + */ + error(msg, position) { + let toast = this.buildToast('danger', msg, position); + + $(this.wormHole).append(toast); + }, + + /** + * Build a toast and animate it + * @param {String} type + * @param {String} msg + * @param {String} position + */ + buildToast(type, msg, position) { + let toast = `
${msg}
`; + + return toast; + }, + + /** + * Clears all toasts in the wormhole + */ + clearAll() { + $(this.wormHole).find('.toast').remove(); + }, + + /** + * Hides a toast in the wormhole + */ + hide() { + $(this.wormHole).find('.toast').removeClass('fadeInDown').addClass('fadeOutUp'); + } +}; \ No newline at end of file diff --git a/app/helpers/utils.js b/app/helpers/utils.js new file mode 100644 index 0000000..58eca77 --- /dev/null +++ b/app/helpers/utils.js @@ -0,0 +1,33 @@ +const $ = require('jquery'); + +/** + * Utils helpers with different methods + */ +module.exports = { + /** + * Normalizes a string + * @param {String} string + */ + normalizeString(string) { + return string.split(/(?=[A-Z])/).join('-').toLowerCase(); + }, + + /** + * Retrives the current layout + */ + getCurrentLayout() { + return $('body').find('section.layout').attr('id'); + }, + + /** + * Adds leading zero to + * @param {String} number + */ + pad(number) { + if (number < 10) { + return `0${number}`; + } else { + return number; + } + } +}; \ No newline at end of file diff --git a/app/index.js b/app/index.js new file mode 100644 index 0000000..1da320b --- /dev/null +++ b/app/index.js @@ -0,0 +1,78 @@ +/*eslint no-unused-vars: ["error", { "args": "none" }]*/ + +// BASE SETUP +// ============================================================================= + +// call the packages we need +const express = require('express'); +const path = require('path'); +const logger = require('morgan'); +const cookieParser = require('cookie-parser'); +const bodyParser = require('body-parser'); +const RateLimit = require('express-rate-limit'); +const helmet = require('helmet'); +const app = express(); + +// ROUTES +// ============================================================================= +app.use('/', require('./routes/index')); +app.use('/api', require('./routes/api')); + +// VIEWS +// ============================================================================= +app.set('views', path.join(__dirname, 'templates')); +app.set('view engine', 'pug'); + +// MISC +// ============================================================================= +// configure app to use bodyParser() +// this will let us get the data from a POST +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); + +app.use(logger('dev')); +app.use(cookieParser()); + +let limiter = new RateLimit({ + windowMs: 1*60*1000, // 1 minute + max: 50, // limit each IP to 50 requests per windowMs + delayMs: 0 // disable delaying - full speed until the max limit is reached +}); + +// apply to all requests +app.use(limiter); +app.use(helmet({ + noCache: false +})); + +// STATIC FILES +// ============================================================================= +app.use('/public', express.static(path.join(__dirname, '../public'))); + +// LOCALS +// ============================================================================= +app.locals.livereload = true; + +// ERROR HANDLER +// ============================================================================= + +// catch 404 and forward to error handler +app.use(function (req, res, next) { + let err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// error handler +app.use(function (err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500).render('error', { + layoutId : 'error' + }); +}); + +module.exports = app; diff --git a/app/models/advanced.js b/app/models/advanced.js new file mode 100644 index 0000000..a45a3ad --- /dev/null +++ b/app/models/advanced.js @@ -0,0 +1,98 @@ +/** + * Required packages + */ +const os = require('os'); +const osName = require('os-name'); +const getos = require('getos'); +const macosRelease = require('macos-release'); +const winRelease = require('win-release'); + +const humem = require('humem'); +const internalIp = require('internal-ip').v4(); +const publicIp = require('public-ip').v4(); +const netmask = require('ipmask')(); + +/** + * Abstract module with all methods + */ +module.exports = { + /** + * Advanced data method. Gathers all advanced data + * and returns is as an object. The data param is optional. + * @param {Object} data + */ + gatherAdvancedData(data = {}) { + return { + os : this.getOS(), + processor : this.parseCPUModel(), + architecture : os.arch(), + totalMem : humem.totalmem, + hostname : os.hostname(), + localIp : data.localIp, + publicIp : data.publicIp, + networkMask : netmask.netmask, + mac : netmask.mac + }; + }, + + /** + * Returns data about OS distribution and release + */ + getOS() { + let getPlatform = os.platform(), + tempDist, dist, release; + + if (getPlatform === 'linux') { + tempDist = getos((e, os) => { return os.dist; }); + release = getos((e, os) => { return os.release; }); + } else if (getPlatform === 'darwin') { + release = macosRelease().version; + } else if (getPlatform === 'win32') { + release = winRelease(); + } + + if (getPlatform === 'darwin' || getPlatform === 'win32') { + tempDist = osName(); + } + + tempDist = tempDist.split(' '); + dist = tempDist[0]; + + return { dist, release }; + }, + + /** + * Parses the CPU model retrived by the os module + */ + parseCPUModel() { + let model = os.cpus()[0].model, + split = model.split('@'), + modelName = split[0].trim(), + frequency = split[1].trim(); + + modelName = modelName.split('-'); + modelName = modelName[0].replace('CPU', '') + .replace(/\(.*?\)/g, ''); + + return `${frequency} ${modelName}`; + }, + + /** + * Retrives the current server internal Ip and external Ip. + * Passes the data using a callback function. + * @param {Function} callback + * @param {Function} next + */ + getIpObject(callback, next) { + publicIp.then(ip => { + let ipObject = { + localIp : internalIp, + publicIp : ip + }; + + if (callback) { + return callback(ipObject); + } + }).catch(next); + } +}; \ No newline at end of file diff --git a/app/models/basic.js b/app/models/basic.js new file mode 100644 index 0000000..9a8bd4a --- /dev/null +++ b/app/models/basic.js @@ -0,0 +1,88 @@ +/** + * Required packages + */ +const osUptime = require('os-uptime')(); +const moment = require('moment'); +const publicIp = require('public-ip').v4(); +const ipLocation = require('iplocation'); +const utils = require('../helpers/utils'); + +/** + * Abstract module with all methods + */ +module.exports = { + /** + * Main data method. Gathers all data and returns + * is as an object. The data param is optional. + * @param {Object} data + */ + gatherData(data = {}) { + return { + uptime : this.getUptime(), + currentDate : this.getCurrentDate(), + activeDate : this.getActiveDate(), + time : this.getTime(), + location : data.location + }; + }, + + /** + * Calculates the current uptime, using the difference + * between the current time and the OS time. + */ + getUptime() { + let diffSeconds = moment().diff(osUptime, 'seconds'), + calcMinutes = diffSeconds / 60, + calcHours = calcMinutes / 60, + days = Math.floor(calcHours / 24), + hours = Math.floor(calcHours - (days * 24)), + minutes = Math.floor(calcMinutes - (days * 60 * 24) - (hours * 60)); + + return { + days : utils.pad(days), + hours : utils.pad(hours), + minutes : utils.pad(minutes) + }; + }, + + /** + * Returns the current date. + */ + getCurrentDate() { + return moment().format('MMMM DD, YYYY'); + }, + + /** + * Returns the date when the server became active. + */ + getActiveDate() { + return moment(osUptime).format('MMMM DD, YYYY'); + }, + + /** + * Returns the current time. + */ + getTime() { + return { + hh : moment().format('hh'), + mm : moment().format('mm'), + p : moment().format('a') + }; + }, + + /** + * Retrives the current location after it receives the + * public IP. Passes the data using a callback function. + * @param {Function} callback + * @param {Function} next + */ + getLocation(callback, next) { + publicIp.then(ip => { + ipLocation(ip, (error, data) => { + if (callback) { + return callback(data); + } + }); + }).catch(next); + } +}; \ No newline at end of file diff --git a/app/routes/api.js b/app/routes/api.js new file mode 100644 index 0000000..e96f685 --- /dev/null +++ b/app/routes/api.js @@ -0,0 +1,41 @@ +/** + * Required packages + */ +const router = require('express').Router(); +const basic = require('../models/basic'); +const advanced = require('../models/advanced'); + +/* GET api endpoint. */ +router.get('/basic', function (req, res, next) { + + if (req.xhr) { + res.json(basic.gatherData()); + } else { + return next(new Error("Permission denied")); + } + +}); + +/* GET advanced api endpoint. */ +router.get('/advanced', function (req, res, next) { + + if (req.xhr) { + + advanced.getIpObject((ipObject) => { + + return res.json( + advanced.gatherAdvancedData({ + localIp : ipObject.localIp, + publicIp : ipObject.publicIp + }) + ); + + }, next); + + } else { + return next(new Error("Permission denied")); + } + +}); + +module.exports = router; diff --git a/app/routes/index.js b/app/routes/index.js new file mode 100644 index 0000000..24e080d --- /dev/null +++ b/app/routes/index.js @@ -0,0 +1,23 @@ +/** + * Required packages + */ +const router = require('express').Router(); +const basic = require('../models/basic'); + +/* GET home page. */ +router.get('/', function (req, res, next) { + + basic.getLocation((location) => { + + return res.render('index', { + layoutId : 'index', + data : basic.gatherData({ + location + }) + }); + + }, next); + +}); + +module.exports = router; \ No newline at end of file diff --git a/app/styles/base/_base.scss b/app/styles/base/_base.scss new file mode 100644 index 0000000..9e27e27 --- /dev/null +++ b/app/styles/base/_base.scss @@ -0,0 +1,66 @@ +// +// Global +// -------------------------------------------------- + +html, +body { + height: 100%; +} + +html { + font-size: $font-size-root; +} + +body { + background-color: $color-black; + font-family: $font-family-base; + font-size: $font-size-base; + line-height: $line-height-base; + font-weight: $font-weight-root; + color: $color-grey1; +} + +::selection { + background: $color-grey1; +} + +a { + color: $color-white; + outline: none; + text-decoration: none; + @include transition('color'); + + &:hover, + &:active, + &:focus, + &:visited, + &:focus:active { + color: $color-white; + outline: none; + } + + &:hover, + &:focus { + text-decoration: underline; + } +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin-bottom: 0; +} + +ul { + list-style-type: none; + margin: 0; + padding: 0; + + li { + margin-top: 0; + margin-bottom: .5rem; + } +} \ No newline at end of file diff --git a/app/styles/base/_colors.scss b/app/styles/base/_colors.scss new file mode 100644 index 0000000..c9d1656 --- /dev/null +++ b/app/styles/base/_colors.scss @@ -0,0 +1,12 @@ +// +// Main colors +// -------------------------------------------------- + +$color-white: #fff; +$color-black: #202020; + +$color-grey1: #eee; + +$color-red1: #e85600; + +$color-green1: #32b643; \ No newline at end of file diff --git a/app/styles/base/_layout.scss b/app/styles/base/_layout.scss new file mode 100644 index 0000000..b957a81 --- /dev/null +++ b/app/styles/base/_layout.scss @@ -0,0 +1,46 @@ +// +// Layout and UI +// -------------------------------------------------- + +.container { + .layout { + max-width: 960px; + margin: 0 auto; + + .blink { + @include animation('blink 2s infinite'); + } + + .loading { + &::after { + border-bottom-color: $color-white; + border-left-color: $color-white; + } + } + + // Fix for Safari's interpretation + // of the the flex specification + .col-flex, + .box-flex { + display: flex; + flex-direction: column; + + .box, + .box-inner-wrapper { + flex: 1; + } + } + } + + .copyright { + max-width: 960px; + margin: 0 auto; + font-size: $font-size-root; + color: rgba($color-white, .6); + cursor: default; + + a { + text-decoration: underline; + } + } +} \ No newline at end of file diff --git a/app/styles/base/_media.scss b/app/styles/base/_media.scss new file mode 100644 index 0000000..d6fbbad --- /dev/null +++ b/app/styles/base/_media.scss @@ -0,0 +1,12 @@ +// +// Media queries +// -------------------------------------------------- + +// Large desktop +@media (min-width: 1281px) { + .container { + .layout { + padding-top: 60px; + } + } +} \ No newline at end of file diff --git a/app/styles/base/_variables.scss b/app/styles/base/_variables.scss new file mode 100644 index 0000000..055da37 --- /dev/null +++ b/app/styles/base/_variables.scss @@ -0,0 +1,23 @@ +// +// Variables +// -------------------------------------------------- + +$font-family-base: 'Source Sans Pro', sans-serif; + +$font-size-root: 12px; +$font-size-base: 12px; + +$font-xs: 13px; +$font-sm: 14px; +$font-md: 16px; +$font-lg: 24px; +$font-xl: 54px; + +$font-weight-root: 300; + +$line-height-base: $font-size-base; + +$line-height-sm: $font-sm; +$line-height-md: $font-md; +$line-height-lg: $font-lg; +$line-height-xl: $font-xl; \ No newline at end of file diff --git a/app/styles/helpers/_mixins.scss b/app/styles/helpers/_mixins.scss new file mode 100644 index 0000000..798bc11 --- /dev/null +++ b/app/styles/helpers/_mixins.scss @@ -0,0 +1,39 @@ +// +// Mixins +// -------------------------------------------------- + +@mixin keyframes($animation-name) { + @keyframes #{$animation-name} { + @content; + } +} + +@mixin animation($str) { + animation: #{$str}; +} + +@include keyframes(blink) { + 0% { opacity: 0; } + 50% { opacity: 1; } + 100% { opacity: 0; } +} + +@mixin transition($type) { + transition: $type .5s ease-in-out; +} + +@mixin absolute-horizontal-center { + left: 50%; + transform: translateX(-50%); +} + +@mixin absolute-vertical-center { + top: 50%; + transform: translateY(-50%); +} + +@mixin absolute-center { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} \ No newline at end of file diff --git a/app/styles/helpers/_notes.scss b/app/styles/helpers/_notes.scss new file mode 100644 index 0000000..7478808 --- /dev/null +++ b/app/styles/helpers/_notes.scss @@ -0,0 +1,51 @@ +// +// Notes +// -------------------------------------------------- + +.note { + position: absolute; + + &::before { + content: ''; + display: block; + border-style: solid; + border-width: 0 14px 14px 0; + } + + &::after { + font-size: $font-size-base; + line-height: $line-height-base; + } + + &.note-tl { + top: 0; + left: 0; + } + + &.note-tr { + top: 0; + right: 0; + } + + &.note-bl { + bottom: 0; + left: 0; + } + + &.note-br { + top: 0; + right: 0; + } + + &.note-success { + &::before { + border-color: transparent $color-green1 transparent transparent; + } + } + + &.note-danger { + &::before { + border-color: transparent $color-red1 transparent transparent; + } + } +} \ No newline at end of file diff --git a/app/styles/helpers/_toasts.scss b/app/styles/helpers/_toasts.scss new file mode 100644 index 0000000..a9ce1f4 --- /dev/null +++ b/app/styles/helpers/_toasts.scss @@ -0,0 +1,43 @@ +// +// Toasts +// -------------------------------------------------- + +.container { + .toasts-wormhole { + position: relative; + + .toast { + position: fixed; + z-index: 999; + width: auto; + + &.toast-success { + background: rgba($color-green1, .3); + } + + &.toast-warning { + background: rgba($color-red1, .3); + } + + &.toast-tl { + top: 10px; + left: 10px; + } + + &.toast-tr { + top: 10px; + right: 10px; + } + + &.toast-bl { + bottom: 10px; + left: 10px; + } + + &.toast-br { + top: 10px; + right: 10px; + } + } + } +} \ No newline at end of file diff --git a/app/styles/helpers/_utils.scss b/app/styles/helpers/_utils.scss new file mode 100644 index 0000000..2dd0cfb --- /dev/null +++ b/app/styles/helpers/_utils.scss @@ -0,0 +1,44 @@ +// +// Util classes +// -------------------------------------------------- + +.icon { + display: inline-block; + vertical-align: text-top; + + .octicon { + fill: currentColor; + } +} + +.mt-0 { + margin-top: 0; +} + +.mr-0 { + margin-right: 0; +} + +.mb-0 { + margin-bottom: 0; +} + +.ml-0 { + margin-left: 0; +} + +.pt-0 { + padding-top: 0; +} + +.pr-0 { + padding-right: 0; +} + +.pb-0 { + padding-bottom: 0; +} + +.pl-0 { + padding-left: 0; +} \ No newline at end of file diff --git a/app/styles/layout/_index.scss b/app/styles/layout/_index.scss new file mode 100644 index 0000000..712c59d --- /dev/null +++ b/app/styles/layout/_index.scss @@ -0,0 +1,121 @@ +// +// Index layout +// -------------------------------------------------- + +.layout { + .box { + position: relative; + cursor: default; + height: 100%; + border: 1px solid rgba($color-white, .2); + padding: 20px 15px; + + .box-inner-wrapper { + height: 100%; + width: 100%; + align-items: center; + justify-content: center; + flex-wrap: wrap; + + section { + width: 100%; + align-self: flex-end; + padding: 10px 0; + + .data-wrapper { + font-size: $font-lg; + line-height: $line-height-lg; + + &.time-content { + > span { + margin: 0 2px; + text-transform: uppercase; + } + } + + &.uptime-content { + > span { + font-size: $font-xl; + line-height: $line-height-xl; + width: 70px; + + label { + display: block; + text-transform: uppercase; + font-size: $font-sm; + line-height: $line-height-sm; + } + } + } + + &.advanced-content { + font-size: $font-sm; + line-height: $line-height-sm; + + ul { + height: 100%; + } + + .list-labels { + color: rgba($color-white, .6); + } + + .list-value { + position: relative; + + .action { + position: absolute; + @include absolute-vertical-center; + cursor: pointer; + @include transition('opacity'); + opacity: 0; + visibility: hidden; + + &::after { + text-transform: capitalize; + font-size: 12px; + line-height: 12px; + } + } + + &:hover { + .action { + opacity: 1; + visibility: visible; + } + } + } + } + + > span { + display: inline-block; + margin: 0 10px; + } + } + } + + footer { + width: 100%; + align-self: flex-end; + padding-top: 10px; + margin-top: 15px; + border-top: 1px solid rgba($color-white, .1); + color: rgba($color-white, .6); + + label { + letter-spacing: 2px; + } + } + } + } + + .section-header { + padding-bottom: 10px; + margin-bottom: 15px; + border-bottom: 1px solid rgba($color-white, .1); + color: rgba($color-white, .6); + font-size: $font-size-base; + line-height: $line-height-base; + letter-spacing: 2px; + } +} \ No newline at end of file diff --git a/app/styles/main.scss b/app/styles/main.scss new file mode 100644 index 0000000..34d8e6f --- /dev/null +++ b/app/styles/main.scss @@ -0,0 +1,23 @@ +// +// The main scss file +// -------------------------------------------------- + +// Variables +@import 'base/variables'; + +// Base files +@import 'base/colors'; +@import 'helpers/mixins'; +@import 'base/base'; + +// Layout files +@import 'base/layout'; +@import 'layout/index'; + +// Utils +@import 'base/media'; +@import 'helpers/utils'; + +// Misc +@import 'helpers/toasts'; +@import 'helpers/notes'; \ No newline at end of file diff --git a/app/templates/error.pug b/app/templates/error.pug new file mode 100644 index 0000000..4d1f911 --- /dev/null +++ b/app/templates/error.pug @@ -0,0 +1,8 @@ +extends layout + +block content + +row + +column(12, 12) + .error-wrapper.text-center + h1=message + h2=error.status \ No newline at end of file diff --git a/app/templates/index.pug b/app/templates/index.pug new file mode 100644 index 0000000..700990b --- /dev/null +++ b/app/templates/index.pug @@ -0,0 +1,48 @@ +extends layout + +block content + +row + +column(5, 12, 'mt-10 mb-10') + +box('lg', 'server uptime', 'uptime') + +output('days', data.uptime.days, 'days') + +output('hours', data.uptime.hours, 'hours') + +output('minutes', data.uptime.minutes, 'minutes') + +column(7, 12) + +row + +column(6, 12) + +box('sm', 'date on server', 'current-date') + +output('current-date', data.currentDate) + +column(6, 12) + +box('sm', 'server is active since', 'active-date') + +output('active-date', data.activeDate) + +column(6, 12) + +box('sm', 'time on server', 'time') + +output('hh', data.time.hh) + span.blink : + +output('mm', data.time.mm) + +output('p', data.time.p) + +column(6, 12) + +box('sm', 'server location', 'location') + +output('location') + if data.location + a(href=`https://www.google.com/maps/place/${data.location.latitude},${ data.location.longitude}` target='_blank') + if data.location.city + | #{data.location.city}, + | #{' ' + data.location.country_name} + +column(12, 12, 'pt-0') + +box('lg', 'advanced info', 'advanced') + +row + +column(6, 12) + .section-header.text-uppercase Server info + +row + +column(6, 6, 'text-right text-uppercase') + +list('' , false, false, 'platform', 'release', 'processor', 'architecture', 'total memory') + +column(6, 6, 'text-left') + +list('server-info', true, true, 'dist', 'release', 'processor', 'architecture', 'total-mem') + +column(6, 12) + .section-header.text-uppercase Network + +row + +column(6, 6, 'text-right text-uppercase') + +list('', false, false, 'hostname', 'local ip', 'public ip', 'network mask', 'mac') + +column(6, 6, 'text-left') + +list('network-info', true, true, 'hostname', 'local-ip', 'public-ip', 'network-mask', 'mac') \ No newline at end of file diff --git a/app/templates/layout.pug b/app/templates/layout.pug new file mode 100644 index 0000000..685eafb --- /dev/null +++ b/app/templates/layout.pug @@ -0,0 +1,10 @@ +include ./scaffolding/mixins +doctype html +html(lang="en") + include ./partials/head + body + .container + .toasts-wormhole + section.layout(id=layoutId) + block content + include ./partials/footer \ No newline at end of file diff --git a/app/templates/partials/footer.pug b/app/templates/partials/footer.pug new file mode 100644 index 0000000..5941782 --- /dev/null +++ b/app/templates/partials/footer.pug @@ -0,0 +1,9 @@ +footer(class=(layoutId === 'error') ? "copyright text-center" : "copyright text-right") + +row + +column(6, 6, 'pt-0 text-left') + .request-timer-wrapper.hide + | Request to server failed. Will retry in #[span.request-timer] seconds. + +column(6, 6, 'pt-0') + | data provided by #[a(href="https://github.com/stefanbc/uptimey" target="_blank") @uptimey]. + +include ./scripts \ No newline at end of file diff --git a/app/templates/partials/head.pug b/app/templates/partials/head.pug new file mode 100644 index 0000000..326d779 --- /dev/null +++ b/app/templates/partials/head.pug @@ -0,0 +1,11 @@ +head + if error + title='uptimey - simple uptime server monitor' + else + title=`uptimey - ${data.uptime.days} days ${data.uptime.hours} hours ${data.uptime.minutes} minutes` + + include ./meta + + -var stylesheets = ["//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600", "/public/styles/vendor.min.css", "/public/styles/uptimey.min.css"]; + each val in stylesheets + link(rel="stylesheet" href=val type="text/css") \ No newline at end of file diff --git a/app/templates/partials/meta.pug b/app/templates/partials/meta.pug new file mode 100644 index 0000000..79432b2 --- /dev/null +++ b/app/templates/partials/meta.pug @@ -0,0 +1,13 @@ +link(rel="apple-touch-icon" sizes="180x180" href="/public/images/apple-touch-icon.png") +link(rel="icon" type="image/png" href="/public/images/favicon-32x32.png" sizes="32x32") +link(rel="icon" type="image/png" href="/public/images/favicon-16x16.png" sizes="16x16") +link(rel="manifest" href="/public/images/manifest.json") +link(rel="mask-icon" href="/public/images/safari-pinned-tab.svg" color="#202020") +link(rel="shortcut icon" href="/public/images/favicon.ico") +meta(name="msapplication-config" content="/public/images/browserconfig.xml") +meta(name="theme-color" content="#ffffff") + +meta(http-equiv="content-type" content="text/html;charset=utf-8") +meta(name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no") +meta(name="description" content="Showcase your server uptime with uptimey!") +meta(http-equiv="X-UA-Compatible" content="IE=edge") \ No newline at end of file diff --git a/app/templates/partials/scripts.pug b/app/templates/partials/scripts.pug new file mode 100644 index 0000000..b9faf58 --- /dev/null +++ b/app/templates/partials/scripts.pug @@ -0,0 +1,6 @@ +-var scripts = ["/public/scripts/uptimey.min.js"]; +each val in scripts + script(src=val type="text/javascript") + +if livereload + script(src="//localhost:35729/livereload.js" type="text/javascript") \ No newline at end of file diff --git a/app/templates/scaffolding/mixins.pug b/app/templates/scaffolding/mixins.pug new file mode 100644 index 0000000..9930ebe --- /dev/null +++ b/app/templates/scaffolding/mixins.pug @@ -0,0 +1,53 @@ +//- Generates a row +mixin row + .columns + if block + block + +//- Generates a column +mixin column(column, small, extraClasses = '') + div(class=`column col-${column} col-md-${small} col-flex ${extraClasses}`) + if block + block + +//- Generates a box +mixin box(size, label, wrapper, extraClasses = '') + div(class=`box rounded box-${size} box-flex ${extraClasses}` id=`${wrapper}-box`) + .box-inner-wrapper.flex.text-center + section.box-content-wrapper + div(class=`data-wrapper ${wrapper}-content`) + if block + block + if label + footer.text-uppercase + label=label + +//- Outputs data from the API +mixin output(id, value, label, advanced = false) + if block + span.output(id=id) + block + else + if label + span.output(id=id) + if advanced + label + | #{label} : + span=value + else + span=value + label=label + else + span.output(id=id)=value + +//- Generates an unordered list +mixin list(id = '', output, enableCopy, ...listItems) + ul(id=id class=output ? "list-values loading" : "list-labels") + each item in listItems + if output + li.list-value + +output(item) + if enableCopy + span.action.copy-action.ml-10 + else + li=item \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..68c16ea --- /dev/null +++ b/bower.json @@ -0,0 +1,28 @@ +{ + "name": "uptimey", + "description": "Beautiful Server Uptime Monitor", + "main": "", + "authors": [ + "\"stefanbc\" Stefan Cosma (http://stefancosma.xyz/)" + ], + "license": "MIT", + "keywords": [ + "server", + "monitor", + "uptime", + "beautiful" + ], + "homepage": "", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ], + "dependencies": { + "normalize-css": "^5.0.0", + "spectre.css": "^0.2.7", + "animate.css": "^3.5.2" + } +} diff --git a/client/bin/app.min.js b/client/bin/app.min.js deleted file mode 100644 index e03832c..0000000 --- a/client/bin/app.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s;q="https://github.com/stefanbc/uptimey",i="./client/lib/models/data.php",h="./client/bin/config.js",r="./client/bin/settings.json",n=require("moment"),k=require("humane"),a="",b="",c="",o=function(a){return k.log(a,{timeout:5e3,baseCls:"humane-libnotify"})},f=function(a){return $(a).addClass("pulse"),$(a).on("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){$(this).removeClass("pulse")})},g=function(a,b,c){return $(a).removeClass(b).addClass(c)},m=function(a){return!isNaN(parseFloat(a))},j=function(a){var b,c;if("number"==typeof a||"boolean"==typeof a)return!1;if("undefined"==typeof a||null===a)return!0;if("undefined"!=typeof a.length)return 0===a.length;b=0;for(c in a)a.hasOwnProperty(c)&&b++;return 0===b},d=function(a){var b,c,d,e,h,j,k,l;switch(j="",a){case"toggle":if(j=$(".toggle-button").attr("data-status"),b=$(".toggle-button").parent().attr("data-position"),"closed"===j)switch($(".button-container").animate((d={},d[""+b]=0,d)),$(".toggle-button").attr("data-status","open"),b){case"top":g(".toggle-button","fa-angle-double-down","fa-angle-double-up");break;case"bottom":g(".toggle-button","fa-angle-double-up","fa-angle-double-down")}else if("open"===j)switch($(".button-container").animate((e={},e[""+b]="-80px",e)),$(".toggle-button").attr("data-status","closed"),b){case"top":g(".toggle-button","fa-angle-double-up","fa-angle-double-down");break;case"bottom":g(".toggle-button","fa-angle-double-down","fa-angle-double-up")}break;case"advanced":f(".advanced-button"),j=$(".advanced-button").attr("data-status"),"default"===j?($(".advanced-button").attr("data-status","advanced"),$(".advanced-button").addClass("active"),$(".default-panel").fadeOut(500),$(".advanced-panel").fadeIn(500),$.ajax(i,{method:"GET",data:{action:"advanced",flag:"advanced"},success:function(a){$(".advanced-panel .top-container").html(a)}})):"advanced"===j&&($(".advanced-button").attr("data-status","default"),$(".advanced-button").removeClass("active"),$(".advanced-panel").fadeOut(500),$(".default-panel").fadeIn(500));break;case"refresh":$(".refresh-button").addClass("fa-spin"),p("uptime","refresh"),p("time","refresh"),p("ping"),setTimeout(function(){$(".refresh-button").removeClass("fa-spin")},1e3);break;case"twitter":f(".twitter-button"),l="",0!==$("#days").attr("data-value")&&(l+=$("#days").attr("data-value")+" days "),0!==$("#hours").attr("data-value")&&(l+=$("#hours").attr("data-value")+" hours "),0!==$("#minutes").attr("data-value")&&(l+=$("#minutes").attr("data-value")+" minutes"),k=l+" server uptime. Can you beat this? via",c="uptimey,devops",window.open("http://twitter.com/share?url="+q+"&text="+k+"&hashtags="+c+"&","twitterwindow","height=450, width=550, top="+($(window).height()/2-225)+", left="+$(window).width()/2+", toolbar=0, location=0, menubar=0, directories=0, scrollbars=0");break;case"google-plus":f(".google-plus-button"),o("Feature still in development");break;case"facebook":f(".facebook-button"),o("Feature still in development");break;case"screenshot":h=$(".screenshot-button"),f(h),h.hasClass("fa-camera")?(h.removeClass("fa-camera").addClass("fa-download"),html2canvas(document.body,{onrendered:function(a){var b,c;b=a.toDataURL("image/png").replace("image/png","image/octet-stream"),h.attr("href",b),c=n().format("DDMMYYYYHHmmss"),h.attr("download","Screenshot_"+c+".png")}})):setTimeout(function(){return h.removeClass("fa-download").addClass("fa-camera"),h.removeAttr("href").removeAttr("download")},3e3);break;case"clear":$.ajax(i,{method:"GET",data:{action:"clear"}})}},s=function(){var a;return a=document.createElement("style"),a.appendChild(document.createTextNode("")),document.head.appendChild(a),a.sheet}(),e=function(a,b,c){"insertRule"in a?a.insertRule(b+"{"+c+"}",a.cssRules.length):"addRule"in a&&a.addRule(b,c,a.cssRules.length)},h=function(){$.ajax(i,{method:"GET",data:{action:"override",flag:"override"},error:function(a){return console.log(a)},success:function(a){var b,c,f,i;if(h=$.parseJSON(a),j(h.background_color)||(b="background-color: "+h.background_color+";"),j(h.font_family)||(b+="font-family: "+h.font_family+";"),j(h.font_color)||(b+="color: "+h.font_color+";"),e(s,"body",b),$.each(h.buttons[0],function(a,b){var c;b===!1&&(c="display: none",e(s,"."+a+"-button",c))}),"advanced"===h.default_view&&d("advanced"),"top"!==h.menu_placement)switch(c="overflow: hidden",e(s,".container",c),$(".button-container").removeClass("top-menu").addClass(h.menu_placement+"-menu"),$(".button-container").attr("data-position",""+h.menu_placement),h.menu_placement){case"bottom":g(".toggle-button","fa-angle-double-down","fa-angle-double-up"),$(".button-container .toggle-button").insertBefore(".button-container .button-block")}h.remove_menu===!0&&(i="display: none",e(s,".button-container",i)),h.show_location===!1&&(f="display: none",e(s,".location-inner",f)),h.show_menu_always===!0&&d("toggle")}})},p=function(d,e){var f;switch(d){case"image":$.ajax(i,{method:"GET",data:{action:d},success:function(a){a=a.split(";"),$("body").css("backgroundImage","url("+a[0]+")")}}),f="Made with Uptimey. ",f+="Image from Unsplash.",$("#copy").html(f);break;case"location":$.ajax(i,{method:"GET",data:{action:d},success:function(d){var e;e="http://ipinfo.io/"+d,$.getJSON(e,function(d){var e;$("#location").text(d.city+", "+d.region+", "+d.country).addClass("fadeIn"),e=d.loc.split(","),$("#location").attr("data-latlong",e[0]+"+"+e[1]),a=d.city+", "+d.region+", "+d.country,$.simpleWeather({location:a,success:function(a){b=a.sunrise,c=a.sunset}})}),$(".location-inner").addClass("fadeIn")}});break;case"uptime":$.ajax(i,{method:"GET",data:{action:d,flag:e},success:function(a){a=a.split(";"),$("#days").text(a[0]).addClass("fadeIn"),$("#days").attr("data-value",a[0]),$("#hours").text(a[1]).addClass("fadeIn"),$("#hours").attr("data-value",a[1]),$("#minutes").text(a[2]).addClass("fadeIn"),$("#minutes").attr("data-value",a[2]),$(".bottom-container").addClass("fadeIn")}});break;case"time":$.ajax(i,{method:"GET",data:{action:d,flag:e},success:function(a){var d;a=a.split(";"),$("#current").text(a[0]).addClass("fadeIn"),d=a[1].split(":"),$("#time").html(d[0]+":"+d[1]).addClass("fadeIn"),$("#since").text(a[2]).addClass("fadeIn"),setTimeout(function(){var d,e,f;d=n(b,"h:m a").format("X"),e=n(c,"h:m a").format("X"),f=n(a[1],"h:m a").format("X"),f>=d&&e>=f?($(".time .fa").removeClass("fa-moon-o fa-circle-o"),$(".time .fa").addClass("fa-sun-o")):($(".time .fa").removeClass("fa-sun-o fa-circle-o"),$(".time .fa").addClass("fa-moon-o"))},3e3),$(".top-container").addClass("fadeIn")}});break;case"ping":$.ajax(i,{method:"GET",data:{action:d},error:function(a,b){return o(b)},success:function(a){return o(a)}})}$(".val").each(function(){$(this).on("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){$(this).removeClass("fadeIn")})})},l=function(){return h(),$(".top-container").addClass("animated"),$(".bottom-container").addClass("animated"),$(".val").addClass("animated"),$(".button").addClass("animated"),p("image"),p("location"),p("uptime"),p("time")},$(function(){l(),setInterval(function(){p("uptime"),p("time")},6e4),setInterval(function(){p("ping")},3e5),$(".button").each(function(){$(this).on("click",function(){var a;a=$(this).attr("data-action"),d(a)})}),$("#location").on("click",function(){var a;a=$(this).attr("data-latlong"),window.location.href="https://www.google.com/maps/place/"+a})})}).call(this); \ No newline at end of file diff --git a/client/bin/config.js b/client/bin/config.js deleted file mode 100644 index 3835832..0000000 --- a/client/bin/config.js +++ /dev/null @@ -1,20 +0,0 @@ -var config = { - production: {}, - development: { - url: '', - mail: { - transport: 'SMTP', - options: { - service: 'Mailgun', - auth: { - user: '', // mailgun username - pass: '' // mailgun password - } - } - }, - server: { - host: '127.0.0.1', - port: '8080' - } - } -} \ No newline at end of file diff --git a/client/bin/css/global.min.css b/client/bin/css/global.min.css deleted file mode 100644 index 706d337..0000000 --- a/client/bin/css/global.min.css +++ /dev/null @@ -1 +0,0 @@ -.button-container.top-menu .button-block:after,.button-container.bottom-menu .button-block:before,.bottom-container section .col:first-child:before,.bottom-container section .col:nth-child(3):before{content:"";height:1px;background:-moz-linear-gradient(left, transparent 0%, #939393 50%, transparent 100%);background:-webkit-gradient(linear, left top, right top, color-stop(0%, transparent), color-stop(50%, #939393), color-stop(100%, transparent));background:-webkit-linear-gradient(left, transparent 0%, #939393 50%, transparent 100%);background:-o-linear-gradient(left, transparent 0%, #939393 50%, transparent 100%);background:-ms-linear-gradient(left, transparent 0%, #939393 50%, transparent 100%);background:linear-gradient(to right, transparent 0%, #939393 50%, transparent 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#00000000', endColorstr='#00000000',GradientType=1 );display:block}a,.button-container .button,.humane,.humane-libnotify{-webkit-transition:all 0.5s ease;-moz-transition:all 0.5s ease;-o-transition:all 0.5s ease;-ms-transition:all 0.5s ease;transition:all 0.5s ease}.button-container.top-menu,.button-container.bottom-menu{left:50%;-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);-o-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}@-webkit-keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}@-moz-keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}@-ms-keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}@-o-keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}@keyframes blink{0%{opacity:0}50%{opacity:1}100%{opacity:0}}html{height:100%}body{height:100%;background-color:#222;background-repeat:no-repeat;background-position:center;background-size:cover;font-family:Raleway,sans-serif;font-weight:200;color:#fff}a{color:#fff;text-decoration:none}a:hover,a:focus{text-decoration:underline}.blink{-webkit-animation:blink 2s infinite;-moz-animation:blink 2s infinite;-ms-animation:blink 2s infinite;-o-animation:blink 2s infinite;animation:blink 2s infinite}.container{background:rgba(0,0,0,0.6);height:100%;width:100%;position:relative}.button-container{position:absolute;text-align:center;font-size:1.5em;z-index:10}.button-container.top-menu{top:-80px}.button-container.top-menu .button-block{margin-top:25px}.button-container.top-menu .button-block:after{margin-top:25px}.button-container.bottom-menu{bottom:-80px}.button-container.bottom-menu .button-block{margin-bottom:25px}.button-container.bottom-menu .button-block:before{margin-bottom:25px}.button-container .button{cursor:pointer;margin:0 30px;position:relative}.button-container .button:after{content:attr(data-action);position:absolute;visibility:hidden;color:#fff;font-family:Raleway,sans-serif;font-weight:200;font-size:0.5em;line-height:30px;text-align:center;background:rgba(0,0,0,0.6);width:100px;height:30px;border-radius:6px}.button-container .button:hover:after{visibility:visible;opacity:1;top:35px;left:50%;margin-left:-50px;z-index:999}.button-container .refresh-button:hover{color:#00ac8a}.button-container .advanced-button.active{color:#e74b3b}.button-container .twitter-button:hover{color:#55acee}.button-container .google-plus-button:hover{color:#dd4b39}.button-container .facebook-button:hover{color:#3B5998}.button-container .screenshot-button{text-decoration:none}.button-container .screenshot-button:hover{color:#f7d61c}.advanced-panel{display:none}.top-container{margin:0 auto;position:absolute;bottom:400px;left:0;width:100%}.top-container section{width:960px;margin:0 auto;position:relative}.top-container section .row{display:block;margin-bottom:20px}.top-container section .row .val{display:block;font-size:2.5em;padding-bottom:10px}.top-container section .block-right{position:absolute;top:0;right:30px}.top-container section .time{padding:20px 0}.top-container section .time .fa{font-size:3em;margin-right:15px}.top-container section .time .fa-sun-o{color:#ffd900}.top-container section .time .fa-moon-o{color:#3498db}.top-container section .time .val{font-size:4em}.top-container section .location{top:-80px;font-size:1.5em;cursor:pointer}.top-container section .location:hover{text-decoration:underline}.top-container section .location .fa{margin-right:15px}.top-container h2{margin:0;font-weight:lighter;font-size:1.1em}.top-container .notif{width:960px;margin:0 auto;position:relative;display:block;font-family:FontAwesome,Raleway,sans-serif;font-weight:200}.top-container .notif:before{padding-right:10px}.top-container .notif a{text-decoration:underline}.bottom-container{text-align:center;position:absolute;bottom:150px;left:0;width:100%}.bottom-container section{width:960px;margin:0 auto}.bottom-container section .col{width:33%;display:inline-block;padding-top:15px}.bottom-container section .col:first-child:before{margin-bottom:25px}.bottom-container section .col:nth-child(3):before{margin-bottom:25px}.bottom-container section .col .val{display:block;font-size:6em;padding-bottom:10px}.bottom-container section .col .label{text-transform:capitalize}.bottom-container h2{font-weight:lighter;font-size:1.1em;margin-bottom:-25px}footer{position:absolute;bottom:10px;right:20px;color:#ddd;font-size:0.7em;text-align:right}footer a{text-decoration:underline}.humane,.humane-libnotify{color:#fff;font-family:Raleway,sans-serif;font-weight:200;font-size:0.8em;text-align:center;background:rgba(0,0,0,0.6);position:fixed;top:10px;right:10px;z-index:100000;opacity:0;filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);width:165px;padding:10px;border-radius:6px;-moz-transform:translateY(-40px);-webkit-transform:translateY(-40px);-ms-transform:translateY(-40px);-o-transform:translateY(-40px);transform:translateY(-40px)}.humane.humane-animate,.humane-libnotify.humane-libnotify-animate{opacity:1;-moz-transform:translateY(0);-webkit-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}.humane.humane-animate,.humane-libnotify.humane-libnotify-js-animate{opacity:1;-moz-transform:translateY(0);-webkit-transform:translateY(0);-ms-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0)}.humane.humane-libnotify-info{background:rgba(0,0,100,0.9)}.humane.humane-libnotify-success{background:rgba(0,100,0,0.9)}.humane.humane-libnotify-error{background:rgba(100,0,0,0.9)}.humane-libnotify.humane-libnotify-info{background:rgba(0,0,100,0.9)}.humane-libnotify.humane-libnotify-success{background:rgba(0,100,0,0.9)}.humane-libnotify.humane-libnotify-error{background:rgba(100,0,0,0.9)} diff --git a/client/bin/settings.json b/client/bin/settings.json deleted file mode 100644 index 35db713..0000000 --- a/client/bin/settings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "background_color" : "", - "background_image" : "", - "buttons": [{ - "refresh" : true, - "advanced" : true, - "twitter" : true, - "google-plus" : false, - "facebook" : false, - "screenshot" : true - }], - "debug_mode" : false, - "default_view" : "default", - "display_timezone" : "Europe/Bucharest", - "font_color" : "", - "font_family" : "", - "menu_placement" : "bottom", - "remove_menu" : false, - "show_am_pm" : true, - "show_location" : true, - "show_menu_always" : false, - "use_24h_clock" : false -} \ No newline at end of file diff --git a/client/lib/controllers/actions.coffee b/client/lib/controllers/actions.coffee deleted file mode 100644 index a1dbb4b..0000000 --- a/client/lib/controllers/actions.coffee +++ /dev/null @@ -1,138 +0,0 @@ -### Button action ### -action = (type) -> - status = '' - switch type - when 'toggle' - # Get the status of the button - status = $('.toggle-button').attr('data-status') - buttonPlacement = $('.toggle-button').parent().attr("data-position") - # Check the status - if status is 'closed' - # Animate the container (bring it down) - $('.button-container').animate "#{buttonPlacement}": 0 - # Change the button status - $('.toggle-button').attr 'data-status', 'open' - # Change the icon - switch buttonPlacement - when 'top' - changeIcon '.toggle-button', 'fa-angle-double-down', 'fa-angle-double-up' - when 'bottom' - changeIcon '.toggle-button', 'fa-angle-double-up', 'fa-angle-double-down' - else if status is 'open' - # Animate the container (bring it up) - $('.button-container').animate "#{buttonPlacement}": '-80px' - # Change the button status - $('.toggle-button').attr 'data-status', 'closed' - # Change the icon - switch buttonPlacement - when 'top' - changeIcon '.toggle-button', 'fa-angle-double-up', 'fa-angle-double-down' - when 'bottom' - changeIcon '.toggle-button', 'fa-angle-double-down', 'fa-angle-double-up' - return - when 'advanced' - # Animated it - animateElement '.advanced-button' - # Get the status of the button - status = $('.advanced-button').attr('data-status') - # Check the state - if status is 'default' - # Show the correct panel and set the button state - $('.advanced-button').attr 'data-status', 'advanced' - $('.advanced-button').addClass 'active' - $('.default-panel').fadeOut 500 - $('.advanced-panel').fadeIn 500 - # Get the data for this panel - $.ajax data, - method : 'GET' - data : - action : 'advanced', - flag : 'advanced' - success: (notice) -> - # Set the data from ajax - $('.advanced-panel .top-container').html notice - return - else if status is 'advanced' - # Show the correct panel and set the button state - $('.advanced-button').attr 'data-status', 'default' - $('.advanced-button').removeClass 'active' - $('.advanced-panel').fadeOut 500 - $('.default-panel').fadeIn 500 - return - when 'refresh' - # Animated it - $('.refresh-button').addClass 'fa-spin' - # Refresh the values - output 'uptime', 'refresh' - output 'time', 'refresh' - output 'ping' - # Stop animation after 1s - setTimeout (-> - $('.refresh-button').removeClass 'fa-spin' - return - ), 1000 - return - when 'twitter' - # Animated it - animateElement '.twitter-button' - # The action - # Get the current uptime - uptime = '' - if $('#days').attr('data-value') isnt 0 - uptime += $('#days').attr('data-value') + ' days ' - if $('#hours').attr('data-value') isnt 0 - uptime += $('#hours').attr('data-value') + ' hours ' - if $('#minutes').attr('data-value') isnt 0 - uptime += $('#minutes').attr('data-value') + ' minutes' - # Set the tweet - text = uptime + ' server uptime. Can you beat this? via' - # Set the hashtag - hashtag = 'uptimey,devops' - # Open the Twitter share window - window.open "http://twitter.com/share?url=#{projectLink}&text=#{text}&hashtags=#{hashtag}&", 'twitterwindow', "height=450, width=550, top=#{$(window).height() / 2 - 225}, left=#{$(window).width() / 2}, toolbar=0, location=0, menubar=0, directories=0, scrollbars=0" - return - when 'google-plus' - # Animated it - animateElement '.google-plus-button' - # The action - notice "Feature still in development" - return - when 'facebook' - # Animated it - animateElement '.facebook-button' - # The action - notice "Feature still in development" - return - when 'screenshot' - screenshotButton = $('.screenshot-button') - # Animated it - animateElement screenshotButton - # Check the button status - if screenshotButton.hasClass('fa-camera') - # Change the button icon - screenshotButton.removeClass('fa-camera').addClass 'fa-download' - # Create an image from canvas - html2canvas document.body, onrendered: (canvas) -> - # Save the canvas to a data URL - dataURL = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream') - # Set the data url on the screenshot button - screenshotButton.attr 'href', dataURL - # Get the current date for the filename - fileName = moment().format('DDMMYYYYHHmmss') - # Set the filename on the screenshot button - screenshotButton.attr 'download', 'Screenshot_' + fileName + '.png' - return - else - setTimeout (-> - # Change the button icon - screenshotButton.removeClass('fa-download').addClass 'fa-camera' - screenshotButton.removeAttr('href').removeAttr 'download' - ), 3000 - return - when 'clear' - # Clear the session - $.ajax data, - method : 'GET' - data : - action : 'clear' - return diff --git a/client/lib/controllers/config.coffee b/client/lib/controllers/config.coffee deleted file mode 100644 index 90f9b49..0000000 --- a/client/lib/controllers/config.coffee +++ /dev/null @@ -1,69 +0,0 @@ -sheet = do -> - # Create the