diff --git a/.gitignore b/.gitignore
index bfec715..ecc8824 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.sass-cache
.vagrant
-node_modules
\ No newline at end of file
+node_modules
+npm-debug.log
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..6770908
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,16 @@
+language: node_js
+node_js:
+ - "5"
+ - "5.1"
+ - "4"
+ - "4.2"
+ - "4.1"
+ - "4.0"
+ - "0.12"
+ - "0.11"
+ - "0.10"
+before_install:
+ - npm install -g grunt-cli
+ - gem install sass
+install: npm install
+before_script: grunt
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index aafdb1c..387f7e7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2014 Stefan Cosma
+Copyright (c) 2016 Stefan Cosma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 4e8e0d8..24de508 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,53 @@
-uptimey
+uptimey
=======
-Are you proud of your server uptime because you put a lot of time into configuring it?
+[![Build Status](https://travis-ci.org/stefanbc/uptimey.svg?branch=dev)](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)
-Showcase your server uptime with **uptimey** - a beautiful Server Uptime Monitor!
+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!
-Just fork the repo on your web server and then access `/uptimey` in your browser. Simple as that!
+Just clone the repo on your web server and then access your server's host followed by `/uptimey`, in your browser. Simple as that!
-**Features**
+Features
+--
-* Background image is the Bing image of the day and it changes automatically!
-* Works on Linux, Windows, Mac OS servers
+* The background image is a random image from [Unsplash](http://unsplash.com)!
+* Works on Linux, Windows, Mac OS servers.
+* Automatically gathers data from the server.
+* Knows if it's nighttime or daytime.
+* Knows your 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.
-![Screenshot](http://i.imgur.com/isg9t8n.png)
+![Screenshot](https://i.imgur.com/BdIzEkg.png)
-Made by [Stefan Cosma](http://coderbits.com/stefanbc)
\ No newline at end of file
+Requirements
+--
+
+* Apache server
+* PHP
+* Access to the Internet
+
+Developers
+--
+
+Make sure you have Node and npm installed. You'll need to have Grunt and Sass installed. Use these commands:
+
+```
+npm install -g grunt-cli
+gem install sass
+```
+
+You can then install all the project dependencies using:
+
+```
+npm install
+```
+
+Available Grunt tasks:
+
+* `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 comming soon).
+
+For local development you can use Vagrant and you can check if the build passes using Travis-CI.
diff --git a/Vagrantfile b/Vagrantfile
index 549d993..df77c06 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -6,6 +6,6 @@ VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "hashicorp/precise32"
- config.vm.provision :shell, path: "testing.sh"
+ 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/client/bin/app.min.js b/client/bin/app.min.js
new file mode 100644
index 0000000..0218ef4
--- /dev/null
+++ b/client/bin/app.min.js
@@ -0,0 +1 @@
+(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r;p="https://github.com/stefanbc/uptimey",h="./client/lib/models/data.php",g="./client/bin/config.js",q="./client/bin/settings.json",m=require("moment"),j=require("humane"),a="",b="",c="",n=function(a){return j.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")})},l=function(a){return!isNaN(parseFloat(a))},i=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,g;switch(d="",a){case"toggle":d=$(".toggle-button").attr("data-status"),"closed"===d?($(".button-container").animate({top:0}),$(".toggle-button").attr("data-status","open"),$(".toggle-button").removeClass("fa-angle-double-down"),$(".toggle-button").addClass("fa-angle-double-up")):"open"===d&&($(".button-container").animate({top:"-80px"}),$(".toggle-button").attr("data-status","closed"),$(".toggle-button").removeClass("fa-angle-double-up"),$(".toggle-button").addClass("fa-angle-double-down"));break;case"advanced":f(".advanced-button"),d=$(".advanced-button").attr("data-status"),"default"===d?($(".advanced-button").attr("data-status","advanced"),$(".advanced-button").addClass("active"),$(".default-panel").fadeOut(500),$(".advanced-panel").fadeIn(500),$.ajax(h,{method:"GET",data:{action:"advanced",flag:"advanced"},success:function(a){$(".advanced-panel .top-container").html(a)}})):"advanced"===d&&($(".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"),o("uptime","refresh"),o("time","refresh"),o("ping"),setTimeout(function(){$(".refresh-button").removeClass("fa-spin")},1e3);break;case"twitter":f(".twitter-button"),g="",0!==$("#days").attr("data-value")&&(g+=$("#days").attr("data-value")+" days "),0!==$("#hours").attr("data-value")&&(g+=$("#hours").attr("data-value")+" hours "),0!==$("#minutes").attr("data-value")&&(g+=$("#minutes").attr("data-value")+" minutes"),e=g+" server uptime. Can you beat this? via",b="uptimey,devops",window.open("http://twitter.com/share?url="+p+"&text="+e+"&hashtags="+b+"&","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"),n("Feature still in development");break;case"facebook":f(".facebook-button"),n("Feature still in development");break;case"screenshot":c=$(".screenshot-button"),f(c),c.hasClass("fa-camera")?(c.removeClass("fa-camera").addClass("fa-download"),html2canvas(document.body,{onrendered:function(a){var b,d;b=a.toDataURL("image/png").replace("image/png","image/octet-stream"),c.attr("href",b),d=m().format("DDMMYYYYHHmmss"),c.attr("download","Screenshot_"+d+".png")}})):setTimeout(function(){return c.removeClass("fa-download").addClass("fa-camera"),c.removeAttr("href").removeAttr("download")},3e3);break;case"clear":$.ajax(h,{method:"GET",data:{action:"clear"}})}},r=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)},g=function(){$.ajax(h,{method:"GET",data:{action:"override",flag:"override"},error:function(a){return console.log(a)},success:function(a){var b,c,f;g=$.parseJSON(a),i(g.background_color)||(b="background-color: "+g.background_color+";"),i(g.font_family)||(b+="font-family: "+g.font_family+";"),i(g.font_color)||(b+="color: "+g.font_color+";"),e(r,"body",b),$.each(g.buttons[0],function(a,b){var c;b===!1&&(c="display: none",e(r,"."+a+"-button",c))}),"advanced"===g.default_view&&d("advanced"),"top"!==g.menu_placement&&$(".button-container").removeClass("top-menu").addClass(g.menu_placement+"-menu"),g.remove_menu===!0&&(f="display: none",e(r,".button-container",f)),g.show_location===!1&&(c="display: none",e(r,".location-inner",c)),g.show_menu_always===!0&&d("toggle")}})},o=function(d,e){var f;switch(d){case"image":$.ajax(h,{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(h,{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(h,{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(h,{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=m(b,"h:m a").format("X"),e=m(c,"h:m a").format("X"),f=m(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(h,{method:"GET",data:{action:d},error:function(a,b){return n(b)},success:function(a){return n(a)}})}$(".val").each(function(){$(this).on("webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend",function(){$(this).removeClass("fadeIn")})})},k=function(){return g(),$(".top-container").addClass("animated"),$(".bottom-container").addClass("animated"),$(".val").addClass("animated"),$(".button").addClass("animated"),o("image"),o("location"),o("uptime"),o("time")},$(function(){k(),setInterval(function(){o("uptime"),o("time")},6e4),setInterval(function(){o("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
new file mode 100644
index 0000000..3835832
--- /dev/null
+++ b/client/bin/config.js
@@ -0,0 +1,20 @@
+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
new file mode 100644
index 0000000..aea440a
--- /dev/null
+++ b/client/bin/css/global.min.css
@@ -0,0 +1 @@
+.button-container .button-block:after,.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}.notice,.button-container{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}.notice{position:absolute;bottom:0;background:rgba(0,0,0,0.7);padding:35px}.container{background:rgba(0,0,0,0.6);height:100%;width:100%;position:relative}.button-container{position:absolute;top:-80px;text-align:center;font-size:1.5em;z-index:10}.button-container .button-block{margin-top:25px}.button-container .button-block:after{margin-top: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
new file mode 100644
index 0000000..a358cdb
--- /dev/null
+++ b/client/bin/settings.json
@@ -0,0 +1,23 @@
+{
+ "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
+}
\ No newline at end of file
diff --git a/client/lib/controllers/actions.coffee b/client/lib/controllers/actions.coffee
new file mode 100644
index 0000000..aaa98e3
--- /dev/null
+++ b/client/lib/controllers/actions.coffee
@@ -0,0 +1,131 @@
+### Button action ###
+action = (type) ->
+ status = ''
+ switch type
+ when 'toggle'
+ # Get the status of the button
+ status = $('.toggle-button').attr('data-status')
+ # Check the status
+ if status is 'closed'
+ # Animate the container (bring it down)
+ $('.button-container').animate top: 0
+ # Change the button status
+ $('.toggle-button').attr 'data-status', 'open'
+ # Change the icon
+ $('.toggle-button').removeClass 'fa-angle-double-down'
+ $('.toggle-button').addClass 'fa-angle-double-up'
+ else if status is 'open'
+ # Animate the container (bring it up)
+ $('.button-container').animate top: '-80px'
+ # Change the button status
+ $('.toggle-button').attr 'data-status', 'closed'
+ # Change the icon
+ $('.toggle-button').removeClass 'fa-angle-double-up'
+ $('.toggle-button').addClass 'fa-angle-double-down'
+ 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
new file mode 100644
index 0000000..49973c4
--- /dev/null
+++ b/client/lib/controllers/config.coffee
@@ -0,0 +1,62 @@
+sheet = do ->
+ # Create the