├── .editorconfig ├── .gitignore ├── README.md ├── bower.json ├── img ├── by-moot.png ├── company.png ├── riotjslogo.png ├── riotjslogo@2x.png ├── tipiirai.jpg └── wallpaper.jpg ├── index.html ├── make.js ├── package.json ├── src ├── api │ ├── admin.js │ ├── backend.js │ ├── promise.js │ ├── spa.js │ ├── test-data.js │ └── user.js ├── ext │ ├── graph.html │ └── graph.js └── ui │ ├── README.md │ ├── customer.js │ ├── customers.js │ ├── login.js │ ├── search.js │ ├── stats.js │ ├── user.js │ ├── util.js │ └── view-switch.js ├── style ├── css3.styl ├── global.styl ├── index.styl ├── login.styl ├── media.styl ├── search.styl ├── vars.styl └── view.styl └── test ├── index.js └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles 2 | # between different editors and IDEs. 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.editorconfig 3 | !.jshintrc 4 | !.gitignore 5 | !.gitmodules 6 | !.travis.yml 7 | *~ 8 | 9 | dist 10 | node_modules 11 | bower_components 12 | Thumbs.db 13 | ehthumbs.db 14 | Desktop.ini 15 | $RECYCLE.BIN/ 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![](https://muut.com/riotjs/demo/img/riotjslogo@2x.png) 3 | 4 | ## Riot.js demo application 5 | 6 | This is a demo single-page demo application made with Riot.js. It's a generic skeleton for an administration panel that shows the core concepts of modular client side development. 7 | 8 | ### Features 9 | - actually something useful, just fork it and extend it to your needs 10 | - plain and minimal UI 11 | - authentication and session management 12 | - small: ~10kb minified, including Riot (4kb gzipped) 13 | - optional caching of server side requests 14 | 15 | 16 | ### Modular architecture 17 | - one global variable `admin` to build modules 18 | - modules can be included in no specific order (*.js) 19 | - modules can be renamed/removed/modified on without breaking the app 20 | - backend agnostic 21 | - easy to maintain and extend, good for multiple developers 22 | 23 | 24 | ### Extendable 25 | - Real API: try for example `admin().load("customers")` on console 26 | - Promise based server calls for more fluent error handling 27 | - Use app name to extend instead of a framework name: `admin(callback)` 28 | 29 | 30 | ### Planned 31 | - real backend (firebase, hosted server, to be decided...) 32 | - realtime channel 33 | - tests for both UI and API layer 34 | - documented API, including events 35 | 36 | 37 | ## Installation 38 | 39 | Hit following commands to run the administration panel on the console 40 | 41 | ``` sh 42 | bower install 43 | npm install 44 | ./make.js gen 45 | open index.html 46 | ./make.js watch 47 | ``` 48 | 49 | You should be able to modify JS and Stylus files and the concatenation and pre-compilation is automatically taken care of. Check make.js for more other targets than `watch`. 50 | 51 | 52 | [Live version](https://muut.com/riotjs/demo/) • 53 | [Riot website](https://muut.com/riotjs/) • 54 | [Building modular applications with Riot.js](https://muut.com/riotjs/docs/) • 55 | [Original Riot blog post](https://muut.com/blog/technology/riotjs-the-1kb-mvp-framework.html) 56 | 57 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riotjs-admin", 3 | "version": "0.0.2", 4 | "homepage": "https://muut.com/riotjs/demo/", 5 | "authors": [ "Tero Piirainen" ], 6 | "description": "Administration panel – A Riot.js demo (The 1kb MVP library)", 7 | "main": "index.html", 8 | "keywords": [ "administration", "panel", "MVP", "MVC", "control panel"], 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components" 14 | ], 15 | "dependencies": { 16 | "jquery": "~1.11.0", 17 | "riotjs": "latest" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /img/by-moot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muut/riotjs-admin/124c708c7aab66e411f12635cc9037fcd3ae142e/img/by-moot.png -------------------------------------------------------------------------------- /img/company.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muut/riotjs-admin/124c708c7aab66e411f12635cc9037fcd3ae142e/img/company.png -------------------------------------------------------------------------------- /img/riotjslogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muut/riotjs-admin/124c708c7aab66e411f12635cc9037fcd3ae142e/img/riotjslogo.png -------------------------------------------------------------------------------- /img/riotjslogo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muut/riotjs-admin/124c708c7aab66e411f12635cc9037fcd3ae142e/img/riotjslogo@2x.png -------------------------------------------------------------------------------- /img/tipiirai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muut/riotjs-admin/124c708c7aab66e411f12635cc9037fcd3ae142e/img/tipiirai.jpg -------------------------------------------------------------------------------- /img/wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muut/riotjs-admin/124c708c7aab66e411f12635cc9037fcd3ae142e/img/wallpaper.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Admin Demo | Riot.js – The 1kb MVP library 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | 23 | 24 | 28 | 29 | Logout 30 | 31 |
32 |
33 | 34 |
35 | 36 | 37 |
38 |

Administration

39 | 43 | 47 |

48 |
49 | 50 | 51 |
52 |

Customers

53 | 54 | 55 |

Users

56 | 57 | 58 |

Posts

59 | 60 |
61 | 62 | 63 |
64 |
65 |
66 | 67 | 68 |
69 | 70 | 71 |
72 | 73 |
74 | 75 | 76 | 81 | 82 | 83 | 84 | 85 | 93 | 94 | 95 | 100 | 101 | 102 | 118 | 119 | 120 | 123 | 124 | 125 | 128 | 129 | 130 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /make.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // grunt is messy, shelljs is not (https://github.com/arturadib/shelljs) 4 | 5 | require('shelljs/make'); 6 | 7 | var gaze = require('gaze'), 8 | stylus = require('stylus'), 9 | header = ";(function(top) {", 10 | footer = '})(typeof top == "object" ? window : exports);'; 11 | 12 | 13 | // initialize repository 14 | function init() { 15 | mkdir("-p", "dist"); 16 | } 17 | 18 | // Make a single file out of everything 19 | function concat() { 20 | 21 | init(); 22 | 23 | // riot.js 24 | // var js = cat("bower_components/riotjs/riot.js"); 25 | var js = cat("../riotjs/riot.js"); 26 | 27 | // api 28 | js += header + cat("src/api/*.js") + footer; 29 | 30 | // ui 31 | js+= cat(["src/ext/*.js", "src/ui/*.js"]); 32 | 33 | // dist 34 | js.to("dist/admin.js"); 35 | 36 | } 37 | 38 | // Test the API on server side (node.js) 39 | target.test = function() { 40 | 41 | init(); 42 | 43 | // generate API files 44 | (header + cat("src/api/*.js") + footer).to("dist/api.js"); 45 | 46 | // run tests 47 | require("./test/index.js"); 48 | } 49 | 50 | 51 | target.lint = function() { 52 | exec("jshint src"); 53 | } 54 | 55 | // return target.test(); 56 | 57 | // Function to compile a single stylus file 58 | function styl(source, target) { 59 | var dir = source.split("/").slice(0, -1).join("/"); 60 | 61 | stylus(cat(source), { compress: true }).include(dir).render(function(err, css) { 62 | if (err) throw err; 63 | css.to(target); 64 | }); 65 | 66 | } 67 | 68 | // concat target 69 | target.concat = concat; 70 | 71 | 72 | // generate application 73 | target.gen = function() { 74 | concat(); 75 | styl("style/index.styl", "dist/style.css"); 76 | cp("-f", "bower_components/jquery/dist/jquery.min.js", "dist"); 77 | }; 78 | 79 | // watch for changes: ./make.js watch 80 | target.watch = function() { 81 | 82 | // scripts 83 | gaze("src/**/*.js", function() { 84 | this.on('all', function(e, file) { 85 | concat(); 86 | }); 87 | }); 88 | 89 | // styles 90 | gaze("style/*.styl", function() { 91 | this.on('changed', function(e, file) { 92 | styl("style/index.styl", "dist/style.css"); 93 | }); 94 | }); 95 | 96 | }; 97 | 98 | 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "riot-admin", 3 | "description": "Administration panel – A Riot.js demo (The 1kb MVP library)", 4 | "version": "1.0.0", 5 | "homepage": "https://muut.com/riotjs/demo/", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/muut/riotjs-admin.git" 9 | }, 10 | "licenses": [ 11 | { 12 | "type": "MIT", 13 | "url": "http://opensource.org/licenses/mit-license.php" 14 | } 15 | ], 16 | "devDependencies": { 17 | "stylus": "latest", 18 | "lodash": "latest", 19 | "jshint": "latest", 20 | "shelljs": "latest", 21 | "gaze": "latest" 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /src/api/admin.js: -------------------------------------------------------------------------------- 1 | 2 | // The admin API 3 | function Admin(conf) { 4 | 5 | var self = riot.observable(this), 6 | backend = new Backend(conf); 7 | 8 | $.extend(self, conf); 9 | 10 | // load a given page from the server 11 | self.load = function(page, fn) { 12 | 13 | self.trigger("before:load", page); 14 | 15 | self.one("load", fn); 16 | 17 | backend.call("load", page, function(view) { 18 | self.trigger("load", view); 19 | }); 20 | 21 | }; 22 | 23 | // ... other API methods goes here 24 | 25 | // same as load("search") 26 | self.search = function(query, fn) { 27 | return backend.call("search", query, fn); 28 | }; 29 | 30 | 31 | // initialization 32 | backend.call("init", conf.page).always(function(data) { 33 | self.user = new User(self, data ? data.user : {}, backend); 34 | self.trigger("ready"); 35 | 36 | }).done(function(data) { 37 | self.trigger("load", data.view); 38 | 39 | }).fail(function() { 40 | 41 | // failed because 42 | self.user.one("login", function(data) { 43 | $.extend(self.user, data.user); 44 | self.trigger("load", data.view); 45 | 46 | }); 47 | 48 | }); 49 | 50 | // on each "page" load 51 | self.on("load", function(view) { 52 | self.trigger("load:" + view.type, view.data, view.path); 53 | self.page = view.type; 54 | }); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/api/backend.js: -------------------------------------------------------------------------------- 1 | 2 | // Fake backend to simulate a real thing 3 | function Backend(conf) { 4 | 5 | var self = this, 6 | cache = {}, 7 | storage = top.localStorage || { sessionId: conf.sessionId }, 8 | debug = conf.debug && typeof console != 'undefined'; 9 | 10 | 11 | // underlying implementation for `call` can change 12 | self.call = function(method, arg, fn) { 13 | 14 | var ret = test_data[method](arg, storage.sessionId), 15 | promise = new Promise(fn); 16 | 17 | // debug message 18 | if (debug) console.info("->", method, arg); 19 | 20 | // configurable caching for the "load" method 21 | if (conf.cache && method == 'load') { 22 | if (cache[arg]) return promise.done(cache[arg]); 23 | cache[arg] = ret; 24 | } 25 | 26 | // session management 27 | if (ret.sessionId) storage.sessionId = ret.sessionId; 28 | else if (method == 'logout') storage.removeItem("sessionId"); 29 | 30 | // fake delay for the call 31 | setTimeout(function() { 32 | if (debug) console.info("<-", ret); 33 | 34 | promise.always(ret); 35 | promise[ret === false ? 'fail' : 'done'](ret); 36 | 37 | }, 400); 38 | 39 | // given callback 40 | promise.done(fn); 41 | 42 | return promise; 43 | 44 | }; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/api/promise.js: -------------------------------------------------------------------------------- 1 | 2 | // A generic promiese interface by using riot.observable 3 | 4 | function Promise(fn) { 5 | var self = riot.observable(this); 6 | 7 | $.map(['done', 'fail', 'always'], function(name) { 8 | self[name] = function(arg) { 9 | return self[$.isFunction(arg) ? 'on' : 'trigger'](name, arg); 10 | }; 11 | }); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/api/spa.js: -------------------------------------------------------------------------------- 1 | 2 | // The ability to split your single-page application (SPA) into loosely-coupled modules 3 | 4 | var instance; 5 | 6 | top.admin = riot.observable(function(arg) { 7 | 8 | // admin() --> return instance 9 | if (!arg) return instance; 10 | 11 | // admin(fn) --> add a new module 12 | if ($.isFunction(arg)) { 13 | top.admin.on("ready", arg); 14 | 15 | // admin(conf) --> initialize the application 16 | } else { 17 | 18 | instance = new Admin(arg); 19 | 20 | instance.on("ready", function() { 21 | top.admin.trigger("ready", instance); 22 | }); 23 | 24 | } 25 | 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /src/api/test-data.js: -------------------------------------------------------------------------------- 1 | 2 | /*jshint multistr:true */ 3 | 4 | // Fake server responses (aka. "fixtures"), 5 | 6 | var customers = $.map([ 7 | 'Acme, inc.', 8 | 'Widget Corp', 9 | '123 Warehousing', 10 | 'Demo Company', 11 | 'Smith and Co.', 12 | 'Foo Bars', 13 | 'ABC Telecom', 14 | 'Fake Brothers', 15 | 'QWERTY Logistics', 16 | 'Demo, inc.', 17 | 'Sample Company', 18 | 'Sample, inc', 19 | 'Acme Corp', 20 | 'Allied Biscuit', 21 | 'Ankh-Sto Associates', 22 | 'Extensive Enterprise', 23 | 'Galaxy Corp', 24 | 'Globo-Chem', 25 | 'Mr. Sparkle', 26 | 'Globex Corporation', 27 | 'LexCorp', 28 | 'LuthorCorp', 29 | 'Praxis Corporation', 30 | 'Sombra Corporation', 31 | 'Sto Plains Holdings' 32 | 33 | ], function(name, i) { 34 | return { name: name, id: i + 1, val: 100 - (i * 4) + (5 * Math.random()) }; 35 | 36 | }); 37 | 38 | function customer(id) { 39 | return $.extend(customers[id - 1], { 40 | 41 | img: 'img/company.png', 42 | joined: (+new Date() - 100000), 43 | email: 'demo@company.it', 44 | 45 | desc: 'Elit hoodie pickled, literally church-key whatever High Life skateboard \ 46 | tofu actually reprehenderit. Id slow-carb asymmetrical accusamus \ 47 | Portland, flannel tempor proident odio esse quis.', 48 | 49 | invoices: $.map([200, 350, 150, 600], function(total, i) { 50 | return { id: i + 1, total: total, time: (+new Date() - 1234567890 * i) }; 51 | }), 52 | 53 | users: users 54 | 55 | }); 56 | 57 | } 58 | 59 | var users = $.map([ 60 | 'Cheryll Egli', 61 | 'Dominque Larocca', 62 | 'Judie Flaugher', 63 | 'Leonard Fason', 64 | 'Lia Monteith', 65 | 'Lindsy Woolard', 66 | 'Rosanna Broadhead', 67 | 'Sharyl Finlayson', 68 | 'Spencer Zeller', 69 | 'Zelda Fazenbaker' 70 | 71 | ], function(name, i) { 72 | return { name: name, id: i + 1, img: 'img/tipiirai.jpg' }; 73 | 74 | }); 75 | 76 | function user(id) { 77 | 78 | return $.extend(users[id - 1], { 79 | username: 'dominique2', 80 | email: 'demo.user@riotjs.com', 81 | joined: (+new Date() - 100000), 82 | 83 | desc: 'Elit hoodie pickled, literally church-key whatever High Life skateboard \ 84 | tofu actually reprehenderit. Id slow-carb asymmetrical accusamus \ 85 | Portland, flannel tempor proident odio esse quis.' 86 | }); 87 | 88 | } 89 | 90 | function graph(multiplier) { 91 | var arr = []; 92 | 93 | for (var i = 0; i < 30; i++) { 94 | arr[i] = Math.random() * multiplier * i; 95 | } 96 | 97 | return arr; 98 | } 99 | 100 | 101 | var test_data = { 102 | 103 | // load new "page" 104 | load: function(path) { 105 | 106 | var els = path.split("/"), 107 | page = els[0]; 108 | 109 | return { 110 | path: path, 111 | type: page || "stats", 112 | data: page == "stats" ? [ graph(100), graph(100), graph(200) ] : 113 | page == "customers" ? customers : 114 | page == "customer" ? customer(els[1]) : 115 | page == "user" ? user(els[1]) : [] 116 | }; 117 | 118 | }, 119 | 120 | // init 121 | init: function(page, sessionId) { 122 | 123 | return !sessionId ? false : { 124 | user: { 125 | email: "joe@riotjs.com", 126 | name: "Joe Rebellous", 127 | username: "riot" 128 | }, 129 | sessionId: sessionId, 130 | view: test_data.load(page || 'stats') 131 | }; 132 | 133 | }, 134 | 135 | search: function(query) { 136 | return users.filter( function(user){ 137 | return user.name.toLowerCase().match(query.toLowerCase()); 138 | }); 139 | }, 140 | 141 | login: function(params) { 142 | return test_data.init(params.page, params.username == 'riot'); 143 | }, 144 | 145 | logout: function() { 146 | return true; 147 | } 148 | 149 | }; 150 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | 2 | // Current user (logged in or anonymous) 3 | function User(app, data, backend) { 4 | 5 | var self = riot.observable(this); 6 | 7 | $.extend(self, data); 8 | 9 | self.login = function(params, fn) { 10 | 11 | self.one("login", fn); 12 | 13 | return backend.call("login", params, function(data) { 14 | self.trigger("login", data); 15 | }); 16 | 17 | }; 18 | 19 | self.logout = function(fn) { 20 | 21 | self.one("logout", fn); 22 | 23 | return backend.call("logout", {}, function(data) { 24 | self.trigger("logout"); 25 | }); 26 | 27 | }; 28 | 29 | } -------------------------------------------------------------------------------- /src/ext/graph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Graph 8 | 9 | 10 | 11 | 28 | 29 | 30 | 31 |

Forums

32 | 33 | 34 |

Users

35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/ext/graph.js: -------------------------------------------------------------------------------- 1 | 2 | // A minimalistic line graph tool (use with extreme care, not tested) 3 | $.fn.graph2 = function(data, color) { 4 | 5 | var graph = this.attr("width", this.parent().width() - 10), 6 | padd = 35, 7 | c = graph[0].getContext("2d"), 8 | max = Math.max.apply(0, data), 9 | width = graph.width(), 10 | height = graph.height(), 11 | len = data.length; 12 | 13 | // re-render? -> clear 14 | c.clearRect (0, 0, width, height); 15 | 16 | function getX(val) { 17 | return ((width - padd) / len) * val + (padd * 1.5); 18 | } 19 | 20 | function getY(val) { 21 | return height - (((height - padd) / max) * val) - padd; 22 | } 23 | 24 | c.strokeStyle = "#999"; 25 | c.font = "12px " + $("body").css("fontFamily"); 26 | c.fillStyle = "#666"; 27 | c.textAlign = "center"; 28 | 29 | // axises 30 | c.lineWidth = 0.5; 31 | c.beginPath(); 32 | c.moveTo(padd, 0); 33 | c.lineTo(padd, height - padd); 34 | c.lineTo(width, height - padd); 35 | c.stroke(); 36 | 37 | // x labels 38 | for(var i = 0; i < len; i++) { 39 | c.fillText(i, getX(i), height - padd + 20); 40 | } 41 | 42 | // y labels 43 | c.textAlign = "right"; 44 | c.textBaseline = "middle"; 45 | 46 | var steps = Math.round(max / 6 / 100) * 100; 47 | 48 | for(i = 0; i < max; i += steps) { 49 | c.fillText(i, padd - 10, getY(i)); 50 | } 51 | 52 | // lines 53 | c.lineWidth = 1; 54 | c.beginPath(); 55 | c.moveTo(getX(0), getY(data[0])); 56 | 57 | for(i = 1; i < len; i ++) { 58 | c.lineTo(getX(i), getY(data[i])); 59 | } 60 | 61 | c.strokeStyle = color; 62 | c.stroke(); 63 | 64 | }; 65 | 66 | -------------------------------------------------------------------------------- /src/ui/README.md: -------------------------------------------------------------------------------- 1 | 2 | The UI is a list of loosely-coupled presenter modules. -------------------------------------------------------------------------------- /src/ui/customer.js: -------------------------------------------------------------------------------- 1 | 2 | // Presenter for single user 3 | admin(function(app) { 4 | 5 | var root = $("#customer-page"), 6 | tmpl = $("#customer-tmpl").html(), 7 | user_tmpl = $("#user-link-tmpl").html(), 8 | invoice_tmpl = $("#invoice-tmpl").html(); 9 | 10 | app.on("load:customer", function(data) { 11 | 12 | data.joined = util.timeformat(data.joined); 13 | root.html(riot.render(tmpl, data)); 14 | 15 | // users 16 | var list = $("#user-list", root); 17 | 18 | $.each(data.users, function(i, el) { 19 | list.append(riot.render(user_tmpl, el)); 20 | }); 21 | 22 | // invoices 23 | list = $("#invoice-list ul", root); 24 | 25 | $.each(data.invoices, function(i, el) { 26 | el.time = util.timeformat(el.time); 27 | list.append(riot.render(invoice_tmpl, el)); 28 | }); 29 | 30 | }); 31 | 32 | }); -------------------------------------------------------------------------------- /src/ui/customers.js: -------------------------------------------------------------------------------- 1 | 2 | // Presenter for customer list 3 | admin(function(app) { 4 | 5 | var root = $("#bars", app.root), 6 | tmpl = $("#bars-tmpl").html(); 7 | 8 | app.on("load:customers", function(view) { 9 | 10 | var max; 11 | 12 | // clear existing data 13 | root.empty(); 14 | 15 | // add new ones 16 | $.each(view, function(i, entry) { 17 | 18 | // first one is the largest 19 | if (!i) max = entry.val; 20 | 21 | entry.width = Math.round(entry.val / max * 100); 22 | 23 | root.append(riot.render(tmpl, entry)); 24 | 25 | }); 26 | 27 | }); 28 | 29 | }); -------------------------------------------------------------------------------- /src/ui/login.js: -------------------------------------------------------------------------------- 1 | 2 | // Login and logout features 3 | 4 | admin(function(app) { 5 | 6 | var user = app.user, 7 | loading = "is-loading"; 8 | 9 | // login 10 | $("#login").submit(function(e) { 11 | e.preventDefault(); 12 | 13 | var el = $(this).addClass("is-loading"); 14 | 15 | user.login({ 16 | username: this.username.value, 17 | password: this.password.value, 18 | page: app.page 19 | 20 | }).fail(function() { 21 | console.info("login failed"); 22 | 23 | }).done(function() { 24 | el.removeClass("is-loading"); 25 | 26 | }); 27 | 28 | }); 29 | 30 | // logout 31 | $("#logout").click(function(e) { 32 | e.preventDefault(); 33 | var el = $(this).addClass("is-loading"); 34 | 35 | user.logout(function() { 36 | el.removeClass("is-loading"); 37 | }); 38 | 39 | }); 40 | 41 | function toggle(is_logged) { 42 | app.root.toggleClass("is-logged", is_logged).toggleClass("is-not-logged", !is_logged); 43 | } 44 | 45 | user.on("login logout", function(type) { 46 | toggle(type == 'login'); 47 | }); 48 | 49 | toggle(!!user.username); 50 | 51 | }); -------------------------------------------------------------------------------- /src/ui/search.js: -------------------------------------------------------------------------------- 1 | 2 | // Search dropdown 3 | admin(function(app) { 4 | 5 | var form = $("#search"), 6 | tmpl = $("#result-tmpl").html(), 7 | results = $("#results"); 8 | 9 | form.on('keyup', function(e) { 10 | 11 | e.preventDefault(); 12 | 13 | var form = $(this), 14 | val = $.trim(this.q.value); 15 | 16 | if (!val) return; 17 | 18 | form.addClass("is-loading"); 19 | 20 | app.search(val, function(arr) { 21 | form.removeClass("is-loading"); 22 | results.empty().show(); 23 | 24 | $.each(arr, function(i, res) { 25 | results.append(riot.render(tmpl, res)); 26 | }); 27 | 28 | }); 29 | 30 | $(document).one("click keypress", function() { 31 | results.hide(); 32 | }); 33 | 34 | }); 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /src/ui/stats.js: -------------------------------------------------------------------------------- 1 | 2 | // Presenter for stats (the line graphs, see ext/graph.js) 3 | admin(function(app) { 4 | 5 | var canvas = $("canvas", app.root), 6 | colors = ['#be0000', '#4cbe00', '#1fadc5']; 7 | 8 | app.on("load:stats", function(stats) { 9 | 10 | $.each(stats, function(i, data) { 11 | canvas.eq(i).graph2(data, colors[i]); 12 | }); 13 | 14 | }); 15 | 16 | }); -------------------------------------------------------------------------------- /src/ui/user.js: -------------------------------------------------------------------------------- 1 | 2 | // Presenter for single user 3 | admin(function(app) { 4 | 5 | var root = $("#user-page"), 6 | tmpl = $("#user-tmpl").html(); 7 | 8 | app.on("load:user", function(data) { 9 | data.joined = util.timeformat(data.joined); 10 | root.html(riot.render(tmpl, data)); 11 | 12 | // not real banning feature on this demo 13 | $("button", root).click(function() { 14 | $(this).text("User is banned!"); 15 | }); 16 | 17 | }); 18 | 19 | }); -------------------------------------------------------------------------------- /src/ui/util.js: -------------------------------------------------------------------------------- 1 | 2 | // List of utility functions for the UI 3 | var util = { 4 | 5 | // date formatting goes to presenter layer, not inside model 6 | timeformat: function(time) { 7 | var d = new Date(time); 8 | return d.getFullYear() + "/" + (d.getMonth() + 1) + "/" + d.getDate(); 9 | } 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /src/ui/view-switch.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Handle view switching, aka. "routing" 4 | The transition effect is done with CSS 5 | */ 6 | admin(function(app) { 7 | 8 | var is_active = "is-active"; 9 | 10 | // 1. select elements from the page to call riot.route(path) 11 | app.root.on("click", "[href^='#/']", function(e) { 12 | 13 | e.preventDefault(); 14 | 15 | var link = $(this); 16 | 17 | // no action 18 | if (link.hasClass(is_active)) return; 19 | 20 | // loading indicator 21 | link.addClass("is-loading"); 22 | 23 | // Riot changes the URL, notifies listeners and takes care of the back button 24 | riot.route(link.attr("href")); 25 | 26 | }); 27 | 28 | 29 | // 2. listen to route clicks and back button 30 | riot.route(function(path) { 31 | 32 | // Call API method to load stuff from server 33 | app.load(path.slice(2)); 34 | 35 | }); 36 | 37 | // 3. Set "is-active" class name for the active page and navi element 38 | app.on("before:load", function() { 39 | 40 | // remove existing class 41 | $("." + is_active).removeClass(is_active); 42 | 43 | 44 | }).on("load", function(view) { 45 | 46 | // set a new class 47 | $("#" + view.type + "-page").add("#" + view.type + "-nav").addClass(is_active); 48 | 49 | // remove loading indicator 50 | $("nav .is-loading").removeClass("is-loading"); 51 | 52 | }); 53 | 54 | }); 55 | 56 | -------------------------------------------------------------------------------- /style/css3.styl: -------------------------------------------------------------------------------- 1 | 2 | /* Avoid using vendor prefixes */ 3 | 4 | vendor-prefixes = webkit moz w3c 5 | 6 | vendor(prop, args, only = null, ignore = null) 7 | for prefix in vendor-prefixes 8 | unless (only and !(prefix in only)) or (ignore and prefix in ignore) 9 | if prefix == w3c 10 | {prop}: args 11 | else 12 | {'-' + prefix + '-' + prop}: args 13 | 14 | opacity(args...) 15 | opacity: args 16 | filter: 'progid:DXImageTransform.Microsoft.Alpha(Opacity=%s)' % round(args[0] * 100) 17 | 18 | background-size() 19 | vendor('background-size', arguments) 20 | 21 | transform() 22 | vendor('transform', arguments) 23 | 24 | border-image() 25 | vendor('border-image', arguments) 26 | 27 | transition() 28 | vendor('transition', arguments) 29 | 30 | box-sizing() 31 | vendor('box-sizing', arguments) 32 | 33 | border-radius() 34 | vendor('border-radius', arguments) 35 | -------------------------------------------------------------------------------- /style/global.styl: -------------------------------------------------------------------------------- 1 | 2 | /* Global stuff: body, links, inputs, header, #main, footer */ 3 | 4 | // quick reset 5 | * 6 | border 0 7 | margin 0 8 | line-height 1 9 | box-sizing border-box 10 | 11 | 12 | body 13 | font-family "Myriad Pro", "Lucida Grande", "Lucida Sans Unicode" 14 | min-width 400px 15 | 16 | template 17 | display none 18 | 19 | a 20 | color blue 21 | text-decoration none 22 | cursor pointer 23 | 24 | &:hover 25 | text-decoration underline 26 | 27 | input 28 | border 1px solid #c 29 | padding u 30 | border-radius 3px 31 | font-size 100% 32 | 33 | button 34 | padding uh u2 35 | background-color blue 36 | color #f 37 | font-size 100% 38 | cursor pointer 39 | border-radius 5em 40 | outline 0 41 | 42 | &:hover 43 | background-color darken(blue, 5) 44 | 45 | &:active 46 | background-color darken(blue, 15) 47 | 48 | 49 | header 50 | background-color #3 51 | padding u 52 | margin-bottom u2 53 | color #e 54 | text-transform uppercase 55 | letter-spacing 2px 56 | font-size 110% 57 | display none 58 | 59 | header div, #main 60 | max-width 800px 61 | margin 0 auto 62 | 63 | header a 64 | font-size 80% 65 | margin-left 1em 66 | color #e 67 | transition color .1s 68 | 69 | &.is-loading 70 | color #5 71 | 72 | &.is-active 73 | color #9 74 | text-decoration none 75 | cursor default 76 | 77 | nav 78 | margin-left 2em 79 | display inline-block 80 | width 54% 81 | 82 | footer 83 | background-color rgba(#313f4c, .8) 84 | width 100% 85 | position fixed 86 | bottom 0 87 | font-size 80% 88 | color #9 89 | padding .6em 0 90 | letter-spacing 5px 91 | text-align center 92 | display none 93 | 94 | a 95 | color #d 96 | letter-spacing 0 97 | -------------------------------------------------------------------------------- /style/index.styl: -------------------------------------------------------------------------------- 1 | 2 | @import "css3" 3 | @import "vars" 4 | @import "global" 5 | @import "login" 6 | @import "view" 7 | @import "search" 8 | @import "media" 9 | -------------------------------------------------------------------------------- /style/login.styl: -------------------------------------------------------------------------------- 1 | 2 | /* Initial login screen */ 3 | 4 | #logo 5 | background-image url("../img/riotjslogo.png") 6 | width 220px 7 | height 83px 8 | position absolute 9 | top u2 10 | left u2 11 | display none 12 | 13 | #login 14 | padding 2em 15 | background-color rgba(#0, .5) 16 | color #e 17 | margin 150px auto 50px 18 | border-radius 4px 19 | display none 20 | transition opacity .3s 21 | 22 | label 23 | display block 24 | margin-bottom u2 25 | font-weight 100 26 | color #c 27 | text-align left 28 | 29 | span 30 | display block 31 | margin-bottom uh 32 | 33 | button 34 | width 60% 35 | font-size 110% 36 | padding u 0 37 | 38 | input 39 | width 100% 40 | border none 41 | background-color rgba(#f, .5) 42 | box-shadow 0 0 5px #4 inset 43 | 44 | &:focus 45 | background-color #f 46 | 47 | h3 48 | font-family "avenir next", "helvetica neue", "verdana" 49 | font-weight 100 50 | font-size 250% 51 | margin-bottom u 52 | 53 | &.is-loading 54 | opacity .5 55 | 56 | 57 | .is-not-logged 58 | background #f url(../img/wallpaper.jpg) 0 0 no-repeat 59 | text-align center 60 | background-size cover 61 | 62 | #login 63 | display inline-block 64 | 65 | #logo, footer 66 | display block 67 | 68 | 69 | .is-logged 70 | background-image none 71 | 72 | header, .page 73 | display block 74 | 75 | #login 76 | display none 77 | -------------------------------------------------------------------------------- /style/media.styl: -------------------------------------------------------------------------------- 1 | 2 | // logo 3 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) 4 | #logo 5 | background-image url("../img/riotjslogo@2x.png") 6 | -webkit-background-size 100% 7 | 8 | // login backgrond image 9 | @media (max-width: 1150px) 10 | body.is-not-logged 11 | background-size 150% 1600px 12 | 13 | @media (max-width: 800px) 14 | #search 15 | display block 16 | margin-bottom u2 17 | 18 | navi 19 | margin-left -(uh) 20 | width 100% 21 | 22 | header a 23 | font-size 12px 24 | 25 | #logout 26 | position absolute 27 | top 20px 28 | right 15px 29 | margin 0 30 | 31 | #bars 32 | em 33 | display block 34 | margin-bottom -(uh) -------------------------------------------------------------------------------- /style/search.styl: -------------------------------------------------------------------------------- 1 | 2 | /* search dropdown */ 3 | 4 | #search 5 | display inline-block 6 | position relative 7 | 8 | input 9 | padding .3em 1em 10 | border-radius 3em 11 | font-size 85% 12 | background-color #6 13 | transition background-color .4s 14 | border-color #6 15 | 16 | &:focus 17 | outline none 18 | background-color #f 19 | 20 | &.is-loading 21 | opacity .5 22 | transition opacity .2s 23 | 24 | #results 25 | position absolute 26 | top 110% 27 | left 0 28 | background-color #f 29 | width 100% 30 | box-shadow 0 0 6px #c 31 | border-radius 4px 32 | display none 33 | z-index 9 34 | 35 | a 36 | font-size 80% 37 | color #3 38 | text-transform none 39 | letter-spacing 0 40 | display block 41 | margin .5em 42 | text-decoration none 43 | 44 | &:hover 45 | background-color blue 46 | color #f 47 | 48 | > * 49 | display inline-block 50 | vertical-align middle 51 | img 52 | width 2.4em 53 | margin-right uh 54 | 55 | span 56 | margin-top uh 57 | -------------------------------------------------------------------------------- /style/vars.styl: -------------------------------------------------------------------------------- 1 | 2 | /* CSS variables for colors and sizing */ 3 | 4 | mootred = #ff003f 5 | red = #be0000 6 | green = #4cbe00 7 | blue = #1fadc5 8 | blue2 = #6cb7c4 9 | pink = #ff57a8 10 | 11 | 12 | // size units 13 | u = 0.6em 14 | u2 = 1.2em 15 | uh = 0.3em -------------------------------------------------------------------------------- /style/view.styl: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | All the different views of this application. 4 | Might make sense to separate into individual files when things start growing. 5 | */ 6 | 7 | // CSS based fading for view switch 8 | #main 9 | position relative 10 | 11 | // pages are absolutely positioned so that they can fade-in/out 12 | .page 13 | position absolute 14 | top 0 15 | left 0 16 | opacity 0 17 | transition opacity .1s 18 | width 100% 19 | z-index -1 20 | padding 15px 21 | display none 22 | 23 | &.is-active 24 | opacity 1 25 | z-index 1 26 | 27 | 28 | // statistical bars 29 | #bars 30 | a 31 | display block 32 | padding uh 33 | text-decoration none 34 | 35 | > * 36 | display inline-block 37 | vertical-align middle 38 | 39 | &:hover 40 | background-color #f9 41 | color #3 42 | 43 | em 44 | width 20% 45 | white-space nowrap 46 | padding-right u 47 | font-size 90% 48 | font-style normal 49 | 50 | span 51 | width 78% 52 | 53 | strong 54 | height 4px 55 | background-color #7 56 | display block 57 | border-radius 4px 58 | 59 | #stats-page 60 | h3 61 | font-weight normal 62 | font-size 100% 63 | margin 0 0 -1.1em 3em 64 | 65 | canvas 66 | height 200px 67 | margin-bottom 25px 68 | 69 | #user-page, #customer-page 70 | margin-top u2 71 | 72 | img 73 | margin-right u2 74 | border-radius 4px 75 | 76 | .details 77 | display inline-block 78 | max-width 530px 79 | vertical-align top 80 | padding-top uh 81 | 82 | h1 83 | font-weight normal 84 | 85 | .meta 86 | margin-bottom u 87 | color #7 88 | margin uh 0 u 89 | 90 | span 91 | margin 0 uh 92 | color #c 93 | 94 | .desc 95 | color #5 96 | font-size 120% 97 | line-height 1.2 98 | margin-bottom u2 99 | 100 | #customer-page 101 | img 102 | max-width 180px 103 | 104 | #user-list 105 | margin-bottom u2 106 | 107 | img 108 | max-width 60px 109 | margin 0 u uh 0 110 | 111 | #invoice-list 112 | max-width 200px 113 | 114 | ul 115 | list-style-type none 116 | padding 0 117 | 118 | li 119 | padding uh 0 120 | 121 | strong 122 | float right 123 | 124 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Shows you how to test your API (model) on the server. 4 | This makes a seamless integration to the development workflow. 5 | */ 6 | 7 | // required stuff for tests 8 | require("./test.js") 9 | 10 | // the admin interface 11 | var admin = require("../dist/api.js").admin; 12 | 13 | // run the tests 14 | admin(function(app) { 15 | 16 | it("Should have proper initial values", function() { 17 | assert.equal(app.page, "stats"); 18 | assert.equal(app.user.username, "riot"); 19 | }); 20 | 21 | it("Should have an initial view", function() { 22 | app.one("load", function(view) { 23 | assert.equal(view.type, "stats"); 24 | }) 25 | }) 26 | 27 | }); 28 | 29 | // initialize 30 | admin({ page: "stats", debug: false, cache: false, sessionId: 'abc' }) 31 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | // BDD style testing 3 | require("../bower_components/riotjs/bdd.js"); 4 | 5 | // jQuery object with Riot functions 6 | global.$ = global.riot = require("../bower_components/riotjs/riot.js"); 7 | 8 | // $ utility functions used by the API (from lodash) 9 | var _ = require("lodash"); 10 | 11 | _.each(['map', 'isFunction', 'extend'], function(name) { 12 | $[name] = _[name]; 13 | }); 14 | --------------------------------------------------------------------------------