├── .bowerrc ├── .gitignore ├── .jshintrc ├── .openshift ├── README.md ├── action_hooks │ └── README.md ├── cron │ ├── README.cron │ ├── daily │ │ └── .gitignore │ ├── hourly │ │ └── .gitignore │ ├── minutely │ │ └── .gitignore │ ├── monthly │ │ └── .gitignore │ └── weekly │ │ ├── README │ │ ├── chrono.dat │ │ ├── chronograph │ │ ├── jobs.allow │ │ └── jobs.deny └── markers │ ├── README.md │ └── hot_deploy ├── .travis.yml ├── Gruntfile.js ├── README.md ├── app ├── favicon.ico ├── images │ └── 404.png ├── index.handlebars ├── magma │ ├── app.js │ └── directives │ │ ├── mgBind_directive.js │ │ ├── mgInclude_directive.js │ │ ├── mgScope_directive.js │ │ ├── mgSubmit_directive.js │ │ └── mgView_directive.js ├── scripts │ ├── app.js │ ├── controllers │ │ └── Header_controller.js │ ├── directives │ │ ├── book_directive.js │ │ └── books_directive.js │ └── services │ │ ├── appInterceptor_service.js │ │ ├── mgViewInterceptor_service.js │ │ └── progressInterceptor_service.js ├── styles │ └── main.scss └── views │ ├── 404.handlebars │ ├── author.handlebars │ ├── book.handlebars │ ├── home.handlebars │ ├── search.handlebars │ └── templates │ ├── book.handlebars │ ├── books.handlebars │ └── pagination.handlebars ├── bower.json ├── ci ├── before_deploy.sh └── build.sh ├── docs ├── architecture.png ├── client.png ├── index.png └── server.png ├── grunt ├── bump.js ├── clean.js ├── copy.js ├── express.js ├── filerev.js ├── imagemin.js ├── jshint.js ├── karma.js ├── ngAnnotate.js ├── open.js ├── sass.js ├── uglify.js ├── usemin.js ├── useminPrepare.js └── watch.js ├── index.js ├── karma.conf.js ├── magma.min.js ├── models ├── 404.js ├── ROUTES.json ├── author.js ├── book.js ├── home.js ├── search.js └── templates │ └── books.js ├── package.json ├── routes ├── root.js └── static.js ├── services └── goodreads.js └── test └── spec └── directives ├── mgBind_directive.spec.js ├── mgInclude_directive.spec.js ├── mgScope_directive.spec.js ├── mgSubmit_directive.spec.js └── mgView_directive.spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "app/components" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .idea 4 | app/components 5 | .tmp 6 | .sass-cache 7 | coverage -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent" : 4, // {int} Number of spaces to use for indentation 16 | "latedef" : false, // true: Require variables/functions to be defined before being used 17 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty" : true, // true: Prohibit use of empty blocks 20 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus" : false, // true: Prohibit use of `++` & `--` 23 | "quotmark" : false, // Quotation mark consistency: 24 | // false : do nothing (default) 25 | // true : ensure whatever is used is consistent 26 | // "single" : require single quotes 27 | // "double" : require double quotes 28 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 29 | "unused" : true, // true: Require all defined variables be used 30 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 31 | "maxparams" : false, // {int} Max number of formal params allowed per function 32 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 33 | "maxstatements" : false, // {int} Max number statements per function 34 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 35 | "maxlen" : false, // {int} Max number of characters per line 36 | 37 | // Relaxing 38 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 39 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 40 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 41 | "eqnull" : false, // true: Tolerate use of `== null` 42 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 43 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 44 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 45 | // (ex: `for each`, multiple try/catch, function expression…) 46 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 47 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 48 | "funcscope" : false, // true: Tolerate defining variables inside control statements 49 | "globalstrict" : true, // true: Allow global "use strict" (also enables 'strict') 50 | "iterator" : false, // true: Tolerate using the `__iterator__` property 51 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 52 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 53 | "laxcomma" : false, // true: Tolerate comma-first style coding 54 | "loopfunc" : false, // true: Tolerate functions being defined in loops 55 | "multistr" : false, // true: Tolerate multi-line strings 56 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 57 | "notypeof" : false, // true: Tolerate invalid typeof operator values 58 | "proto" : false, // true: Tolerate using the `__proto__` property 59 | "scripturl" : false, // true: Tolerate script-targeted URLs 60 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 61 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 62 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 63 | "validthis" : false, // true: Tolerate using this in a non-constructor function 64 | 65 | // Environments 66 | "browser" : true, // Web Browser (window, document, etc) 67 | "browserify" : false, // Browserify (node.js code in the browser) 68 | "couch" : false, // CouchDB 69 | "devel" : true, // Development/debugging (alert, confirm, etc) 70 | "dojo" : false, // Dojo Toolkit 71 | "jasmine" : false, // Jasmine 72 | "jquery" : false, // jQuery 73 | "mocha" : true, // Mocha 74 | "mootools" : false, // MooTools 75 | "node" : false, // Node.js 76 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 77 | "prototypejs" : false, // Prototype and Scriptaculous 78 | "qunit" : false, // QUnit 79 | "rhino" : false, // Rhino 80 | "shelljs" : false, // ShellJS 81 | "worker" : false, // Web Workers 82 | "wsh" : false, // Windows Scripting Host 83 | "yui" : false, // Yahoo User Interface 84 | 85 | // Custom Globals 86 | "globals" : { 87 | "process": true, 88 | "__dirname": true, 89 | "console": true, 90 | "require": true, 91 | "module": true, 92 | "angular": true, 93 | "describe": true, 94 | "expect": true, 95 | "it": true, 96 | "inject": true, 97 | "beforeEach": true, 98 | "afterEach": true, 99 | "spyOn": true 100 | } // additional predefined global variables 101 | } -------------------------------------------------------------------------------- /.openshift/README.md: -------------------------------------------------------------------------------- 1 | For information about .openshift directory, consult the documentation: 2 | 3 | http://openshift.github.io/documentation/oo_user_guide.html#the-openshift-directory 4 | -------------------------------------------------------------------------------- /.openshift/action_hooks/README.md: -------------------------------------------------------------------------------- 1 | For information about action hooks, consult the documentation: 2 | 3 | http://openshift.github.io/documentation/oo_user_guide.html#action-hooks 4 | -------------------------------------------------------------------------------- /.openshift/cron/README.cron: -------------------------------------------------------------------------------- 1 | Run scripts or jobs on a periodic basis 2 | ======================================= 3 | Any scripts or jobs added to the minutely, hourly, daily, weekly or monthly 4 | directories will be run on a scheduled basis (frequency is as indicated by the 5 | name of the directory) using run-parts. 6 | 7 | run-parts ignores any files that are hidden or dotfiles (.*) or backup 8 | files (*~ or *,) or named *.{rpmsave,rpmorig,rpmnew,swp,cfsaved} 9 | 10 | The presence of two specially named files jobs.deny and jobs.allow controls 11 | how run-parts executes your scripts/jobs. 12 | jobs.deny ===> Prevents specific scripts or jobs from being executed. 13 | jobs.allow ===> Only execute the named scripts or jobs (all other/non-named 14 | scripts that exist in this directory are ignored). 15 | 16 | The principles of jobs.deny and jobs.allow are the same as those of cron.deny 17 | and cron.allow and are described in detail at: 18 | http://docs.redhat.com/docs/en-US/Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/ch-Automating_System_Tasks.html#s2-autotasks-cron-access 19 | 20 | See: man crontab or above link for more details and see the the weekly/ 21 | directory for an example. 22 | 23 | PLEASE NOTE: The Cron cartridge must be installed in order to run the configured jobs. 24 | 25 | For more information about cron, consult the documentation: 26 | http://openshift.github.io/documentation/oo_cartridge_guide.html#cron 27 | http://openshift.github.io/documentation/oo_user_guide.html#cron 28 | -------------------------------------------------------------------------------- /.openshift/cron/daily/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/.openshift/cron/daily/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/hourly/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/.openshift/cron/hourly/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/minutely/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/.openshift/cron/minutely/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/monthly/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/.openshift/cron/monthly/.gitignore -------------------------------------------------------------------------------- /.openshift/cron/weekly/README: -------------------------------------------------------------------------------- 1 | Run scripts or jobs on a weekly basis 2 | ===================================== 3 | Any scripts or jobs added to this directory will be run on a scheduled basis 4 | (weekly) using run-parts. 5 | 6 | run-parts ignores any files that are hidden or dotfiles (.*) or backup 7 | files (*~ or *,) or named *.{rpmsave,rpmorig,rpmnew,swp,cfsaved} and handles 8 | the files named jobs.deny and jobs.allow specially. 9 | 10 | In this specific example, the chronograph script is the only script or job file 11 | executed on a weekly basis (due to white-listing it in jobs.allow). And the 12 | README and chrono.dat file are ignored either as a result of being black-listed 13 | in jobs.deny or because they are NOT white-listed in the jobs.allow file. 14 | 15 | For more details, please see ../README.cron file. 16 | 17 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/chrono.dat: -------------------------------------------------------------------------------- 1 | Time And Relative D...n In Execution (Open)Shift! 2 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/chronograph: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "`date`: `cat $(dirname \"$0\")/chrono.dat`" 4 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/jobs.allow: -------------------------------------------------------------------------------- 1 | # 2 | # Script or job files listed in here (one entry per line) will be 3 | # executed on a weekly-basis. 4 | # 5 | # Example: The chronograph script will be executed weekly but the README 6 | # and chrono.dat files in this directory will be ignored. 7 | # 8 | # The README file is actually ignored due to the entry in the 9 | # jobs.deny which is checked before jobs.allow (this file). 10 | # 11 | chronograph 12 | 13 | -------------------------------------------------------------------------------- /.openshift/cron/weekly/jobs.deny: -------------------------------------------------------------------------------- 1 | # 2 | # Any script or job files listed in here (one entry per line) will NOT be 3 | # executed (read as ignored by run-parts). 4 | # 5 | 6 | README 7 | 8 | -------------------------------------------------------------------------------- /.openshift/markers/README.md: -------------------------------------------------------------------------------- 1 | For information about markers, consult the documentation: 2 | 3 | http://openshift.github.io/documentation/oo_user_guide.html#markers 4 | -------------------------------------------------------------------------------- /.openshift/markers/hot_deploy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/.openshift/markers/hot_deploy -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - npm install -g grunt-cli bower 6 | - gem install sass 7 | - npm install && bower install 8 | script: 9 | - npm run build 10 | branches: 11 | only: 12 | - master 13 | before_deploy: 14 | - npm run before_deploy 15 | deploy: 16 | provider: openshift 17 | skip_cleanup: true 18 | user: cool.villi@gmail.com 19 | password: 20 | secure: R7xHX/PC/is4ihGdFFo/kXGEi7A9e5rAZN5byZjTVFIxgzqCtn2H9E/ocfANRkO3LKPyg4zCT9safK9JudTucHkwQwycmkEXR6em4BBWG3zpWDks8dUNk9es4RDCobEP3WyZ3OZH3eyOiG/+3H4lbOQ9ECHDUiTUaammtVOHAps= 21 | app: magma 22 | domain: vilmosioo 23 | on: 24 | repo: vilmosioo/magma 25 | branch: master 26 | env: 27 | global: 28 | - secure: eLszokkjZWQEGgK5Fm3dWriftasGvOjlLoQyBAcNGth2IG6az7sW22w/xCqCAbks1nUvcZFMxeRFofeusUoIvum8t5xEr3bpfNAjHvdT63qQf9zxlH2S32yTgujbwb6ZBeoowH0t6EGD7AZqh5U3WfTJe8FWn9A5+DpRfL0zPvI= -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | // Load grunt tasks automatically 6 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 7 | 8 | var pck = grunt.file.readJSON('./package.json'); 9 | 10 | // Define the configuration for all the tasks 11 | grunt.initConfig({ 12 | pck: pck, 13 | clean: require('./grunt/clean'), 14 | copy: require('./grunt/copy'), 15 | express: require('./grunt/express'), 16 | watch: require('./grunt/watch'), 17 | open: require('./grunt/open'), 18 | useminPrepare: require('./grunt/useminPrepare'), 19 | usemin: require('./grunt/usemin'), 20 | filerev: require('./grunt/filerev'), 21 | sass: require('./grunt/sass'), 22 | ngAnnotate: require('./grunt/ngAnnotate'), 23 | jshint: require('./grunt/jshint'), 24 | imagemin: require('./grunt/imagemin'), 25 | bump: require('./grunt/bump'), 26 | uglify: require('./grunt/uglify'), 27 | karma: require('./grunt/karma') 28 | }); 29 | 30 | grunt.registerTask('test', [ 31 | 'jshint', 32 | 'karma' 33 | ]); 34 | 35 | grunt.registerTask('server', [ 36 | 'clean:server', 37 | 'sass:server', 38 | 'express:server', 39 | 'open', 40 | 'watch' 41 | ]); 42 | 43 | grunt.registerTask('build', [ 44 | 'clean:dist', 45 | 'sass:dist', 46 | 'useminPrepare', 47 | 'concat', 48 | 'ngAnnotate', 49 | 'uglify', 50 | 'copy:magma', 51 | 'cssmin', 52 | 'filerev', 53 | 'copy:static', 54 | 'imagemin', 55 | 'usemin' 56 | ]); 57 | 58 | grunt.registerTask('dist', [ 59 | 'test', 60 | 'build', 61 | 'express:dist' 62 | ]); 63 | 64 | grunt.registerTask('default', [ 65 | 'test', 66 | 'build' 67 | ]); 68 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magma [![Build Status](https://travis-ci.org/vilmosioo/magma.svg?branch=master)](https://travis-ci.org/vilmosioo/magma) 2 | 3 | ### A new way of building web apps by taking advantage of both server and client side templating. 4 | 5 | ## Working Demo 6 | 7 | [http://magma-vilmosioo.rhcloud.com/](http://magma-vilmosioo.rhcloud.com/) 8 | 9 | A web-app that allows you to browse books and authors. 10 | 11 | ## Context 12 | 13 | Complex websites have multiple sources to generate the data. A modular architecture allows you to aggregate different components before sending the final document to the user. Let's assume our website's wireframe looks like this. A, B, C, D are separate components built to display various data. 14 | 15 | ![Magma](docs/index.png) 16 | 17 | Server-side rendering is the common approach to deliver a document. A user makes a request, the server identifies the resources requested, builds the document and sends it back. 18 | 19 | ![Magma](docs/server.png) 20 | 21 | Client-side architecture works in a similar way, except the templating engine is the browser itself. The components are built by JavaScript. 22 | 23 | ![Magma](docs/client.png) 24 | 25 | Client vs server templating is an on-going debate today. Magma suggests a hybrid approach, taking the advantages of both and disadvantages of neither. 26 | 27 | ## How it works 28 | 29 | Magma is not a framework. You don't need any code from this repository. it does not require you to use a particular tech stack. 30 | 31 | It is an architecture. It leverages the main content to the server for fast delivery, while everything else is rendered on the client. 32 | 33 | For example, referencing the wireframe above, your home page contains 4 components: A, B, C, D. On page load, the server will render A and B and send the document to the client. At this point the website is viewable and useful to the user. Once your JavaScript loads, components C and D are loaded using a client rendering engine. After this point the website behaves as a single page application. If you need to render A and B on the client later on, you may call their individual endpoints. 34 | // example code from demo 35 | 36 | ![Magma](docs/architecture.png) 37 | 38 | **TLDR** Magma is an architecture that requires the main content to be delivered by the server on page load, and initialising a single page app as soon as JS is loaded. Components should be exposed by individual endpoints. 39 | 40 | ## Magma AngularJS module 41 | 42 | ``` 43 | npm install magma --save 44 | ``` 45 | 46 | ``` 47 | bower install magma --save 48 | ``` 49 | 50 | ### mgView 51 | 52 | mgView directive allows you to delay route initialisation in your angular application. On `$routechangeSuccess` mgView will replace itself with the standard ngView and the application will behave as a regular SPA. 53 | 54 | ``` 55 |
56 | Server side content that will only get updated after the first $onRouteChangeSuccess event 57 |
58 | ``` 59 | 60 | ### mgSubmit 61 | 62 | To allow your forms to work before JS is loaded (or if a grievous error happened during bootstrap) you should include method and action attributes to it. This allows standard form functionality to work. As soon as angular is ready, mgSubmit will replace itself with the standard ngSubmit. 63 | 64 | ``` 65 |
66 | 67 | ... 68 |
69 | ``` 70 | 71 | ### mgInclude & mgBind 72 | 73 | The problem with ngInclude and ngBind is that they will replace your content immediately after bootstrap, even if data is already exist. mgInclude and mgBind allows you to display persistent server side content, until client side templateing is necessary. 74 | 75 | ``` 76 |
77 | Server side content that will be replaced only when template is defined to a truthy value. 78 |
79 | ``` 80 | 81 | ### mgScope 82 | 83 | mgScope is a very simple directive that allows you to extend an element's scope using a stringified object. 84 | 85 | ``` 86 |
87 | ``` 88 | 89 | In the example above, the element's scope will have a new property called numberOfResults that equals 10. 90 | 91 | ## Developers 92 | 93 | To run the demo, first install it on your computer. 94 | 95 | ``` 96 | git clone https://github.com/vilmosioo/magma.git 97 | cd magma 98 | npm install && bower install 99 | ``` 100 | 101 | The following grunt tasks are made available 102 | 103 | * `grunt server` - Fires an express instance on port 9000 on your local machine, in development mode (CSS/JS is not minified, view caching disabled, angular debug mode is true). 104 | * `grunt dist` - Fires an express instance on port 9000 on your local machine, in production mode (CSS/JS is minified, view caching enabled, angular debug mode is false) 105 | * `grunt build` - Generates the artefacts. 106 | * `grunt test` - Runs the tests 107 | 108 | ## Demo roadmap 109 | 110 | - [x] Search books 111 | - [x] Paginate search results 112 | - [x] View individual book 113 | - [x] View similar books 114 | - [x] View author details 115 | - [ ] Sign-in with Goodreads 116 | - [ ] View your collection 117 | - [ ] Enable grid and list view 118 | 119 | ## Contributing 120 | 121 | ## Attributions 122 | 123 | Kindly hosted by [Openshift](https://www.openshift.com/). 124 | Data provided by [Goodreads](https://www.goodreads.com/). -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/app/favicon.ico -------------------------------------------------------------------------------- /app/images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/app/images/404.png -------------------------------------------------------------------------------- /app/index.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{app.title}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 44 |
45 | 46 |
47 |
48 |
49 | {{{body}}} 50 |
51 |
52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 92 | 93 | -------------------------------------------------------------------------------- /app/magma/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('Magma', []); -------------------------------------------------------------------------------- /app/magma/directives/mgBind_directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('Magma') 4 | .directive('mgBind', function($rootScope, $compile){ 5 | return { 6 | restrict: 'A', 7 | priority: 1000, // We set our custom directive's priority to a high number to ensure that it will be compiled first and with terminal: true, the other directives will be skipped after this directive is compiled. 8 | link: function(scope, el, attr){ 9 | var unbind = scope.$watch(function(){ 10 | return scope.$eval(attr.mgBind); 11 | }, function(value){ 12 | if(value){ 13 | el.attr('ng-bind', attr.mgBind); 14 | el.removeAttr('mg-bind'); 15 | $compile(el)(scope); 16 | unbind(); 17 | } 18 | }); 19 | } 20 | }; 21 | }); -------------------------------------------------------------------------------- /app/magma/directives/mgInclude_directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('Magma') 4 | .directive('mgInclude', function($compile, $animate){ 5 | return { 6 | restrict: 'A', 7 | link: function(scope, el, attr){ 8 | var unbind = scope.$watch(function(){ 9 | return scope.$eval(attr.mgInclude); 10 | }, function(value){ 11 | if(value){ 12 | var view = el.clone().empty(); 13 | view.attr('ng-include', attr.mgInclude); 14 | view.removeAttr('mg-include'); 15 | el.after(view); 16 | $animate.leave(el); 17 | $compile(view)(scope); 18 | unbind(); 19 | } 20 | }); 21 | } 22 | }; 23 | }); -------------------------------------------------------------------------------- /app/magma/directives/mgScope_directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('Magma') 4 | .directive('mgScope', function(){ 5 | return { 6 | restrict: 'A', 7 | link: function(scope, el, attr){ 8 | try{ 9 | angular.extend(scope, JSON.parse(attr.mgScope)); 10 | }catch(err){ 11 | console.error('mgScope requires valid JSON to parse'); 12 | } 13 | } 14 | }; 15 | }); -------------------------------------------------------------------------------- /app/magma/directives/mgSubmit_directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('Magma') 4 | .directive('mgSubmit', function($compile){ 5 | return { 6 | restrict: 'A', 7 | replace: false, 8 | terminal: true, 9 | priority: 1000, // We set our custom directive's priority to a high number to ensure that it will be compiled first and with terminal: true, the other directives will be skipped after this directive is compiled. 10 | link: function(scope, el, attr){ 11 | el.removeAttr('action'); 12 | el.removeAttr('method'); 13 | el.attr('ng-submit', attr.mgSubmit); 14 | el.removeAttr('mg-submit'); 15 | $compile(el)(scope); 16 | } 17 | }; 18 | }); -------------------------------------------------------------------------------- /app/magma/directives/mgView_directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('Magma') 4 | .directive('mgView', function($animate, $rootScope, $compile){ 5 | return { 6 | restrict: 'A', 7 | replace: false, 8 | priority: 1000, // We set our custom directive's priority to a high number to ensure that it will be compiled first and with terminal: true, the other directives will be skipped after this directive is compiled. 9 | link: function(scope, el){ 10 | var enabled; 11 | var unbind = scope.$on('$routeChangeSuccess', function(){ 12 | if(enabled){ 13 | var view = el.clone().empty(); 14 | view.removeAttr('mg-view'); 15 | view.attr('ng-view', ''); 16 | el.after(view); 17 | $animate.leave(el); 18 | $compile(view)(scope); 19 | unbind(); 20 | } 21 | enabled = true; 22 | }); 23 | } 24 | }; 25 | }); -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('MagmaDemo', ['ui.bootstrap', 'ngRoute', 'ngAnimate', 'Magma']) 4 | .config(function($locationProvider, $httpProvider, $routeProvider, ROUTES){ 5 | $locationProvider.html5Mode({ 6 | enabled: true, 7 | requireBase: false 8 | }); 9 | 10 | for(var route in ROUTES){ 11 | if(ROUTES.hasOwnProperty(route)){ 12 | $routeProvider.when(route, ROUTES[route]); 13 | } 14 | } 15 | 16 | $routeProvider 17 | .otherwise({ 18 | templateUrl: '/views/404.html' 19 | }); 20 | 21 | $httpProvider.interceptors.push('mgViewInterceptor'); 22 | $httpProvider.interceptors.push('progressInterceptor'); 23 | $httpProvider.interceptors.push('appInterceptor'); 24 | }) 25 | .run(function($route, $rootScope, $window){ 26 | $rootScope.app = { 27 | title: '', 28 | description: '' 29 | }; 30 | 31 | $rootScope.$on('$routeChangeSuccess', function(){ 32 | $window.scrollTo(0, 0); 33 | }); 34 | }); -------------------------------------------------------------------------------- /app/scripts/controllers/Header_controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('MagmaDemo') 4 | .controller('Header_controller', function($scope, $location, $routeParams){ 5 | $scope.models = { 6 | q: $routeParams.q || '' 7 | }; 8 | 9 | $scope.$watch(function(){ 10 | return $routeParams.q; 11 | }, function(value){ 12 | $scope.models.q = value; 13 | }); 14 | 15 | $scope.handlers = { 16 | search: function(){ 17 | if($scope.SearchForm.$valid){ 18 | $location.search({q: $scope.models.q}).path('/search/'); 19 | } 20 | } 21 | }; 22 | }); -------------------------------------------------------------------------------- /app/scripts/directives/book_directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('MagmaDemo') 4 | .directive('book', function(){ 5 | return { 6 | restrict: 'A', 7 | scope: {} 8 | }; 9 | }); 10 | -------------------------------------------------------------------------------- /app/scripts/directives/books_directive.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('MagmaDemo') 4 | .directive('books', function($location){ 5 | return { 6 | restrict: 'A', 7 | scope: true, 8 | controller: function($scope){ 9 | var params = $location.search(), 10 | current = params.page || 1; 11 | 12 | $scope.templates = {}; 13 | 14 | $scope.$watch('pagination.current', function(page){ 15 | if(page && current !== page){ 16 | // todo make this search agnostic 17 | if(page === 1){ 18 | $location.search({q: params.q}); 19 | $scope.templates.books = '/views/templates/books.html?limit=999&q=' + params.q; 20 | } else { 21 | $location.search(angular.extend(params, {page: page})); 22 | $scope.templates.books = '/views/templates/books.html?limit=999&q=' + params.q + '&page=' + page; 23 | } 24 | 25 | } 26 | }); 27 | } 28 | }; 29 | }); -------------------------------------------------------------------------------- /app/scripts/services/appInterceptor_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('MagmaDemo') 4 | .factory('appInterceptor', function($rootScope){ 5 | 6 | $rootScope.$on('$locationChangeSuccess', function(ev, location){ 7 | $rootScope.app.url = location; 8 | }); 9 | 10 | return { 11 | response: function(res) { 12 | if(res.headers('x-app-title') || res.headers('x-app-description') || res.headers('x-app-image')){ 13 | $rootScope.app.title = res.headers('x-app-title'); 14 | $rootScope.app.description = res.headers('x-app-description'); 15 | $rootScope.app.image = res.headers('x-app-image'); 16 | } 17 | return res; 18 | } 19 | }; 20 | }); -------------------------------------------------------------------------------- /app/scripts/services/mgViewInterceptor_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('Magma') 4 | .factory('mgViewInterceptor', function(ROUTES, $injector){ 5 | var templates = Object.keys(ROUTES).map(function(key){ 6 | return ROUTES[key].templateUrl; 7 | }); 8 | 9 | return { 10 | request: function(config) { 11 | // routeParams is not set yet, we must get the current route instead 12 | var $route = $injector.get('$route'); 13 | if(templates.indexOf(config.url) !== -1){ 14 | config.url += '?' + Object.keys($route.current.params).map(function(key){ 15 | return encodeURIComponent(key) + '=' + encodeURIComponent($route.current.params[key]); 16 | }).join('&'); 17 | } 18 | return config; 19 | } 20 | }; 21 | }); -------------------------------------------------------------------------------- /app/scripts/services/progressInterceptor_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('MagmaDemo') 4 | .factory('progressInterceptor', function($rootScope, $timeout){ 5 | 6 | $rootScope.http = { 7 | progress: 0, 8 | count: 0, 9 | loading: false 10 | }; 11 | 12 | var timeout; 13 | $rootScope.$watch('http.loading', function(value){ 14 | if(value){ 15 | timeout = $timeout(function(){ 16 | $rootScope.http.progress = 100; 17 | }, 100); 18 | } else { 19 | $timeout.cancel(timeout); 20 | $rootScope.http.progress = 0; 21 | } 22 | }); 23 | 24 | return { 25 | request: function(config) { 26 | $rootScope.http.count++; 27 | $rootScope.http.loading = true; 28 | return config; 29 | }, 30 | response: function(response){ 31 | $rootScope.http.count--; 32 | if(!$rootScope.http.count){ 33 | $rootScope.http.loading = false; 34 | } 35 | return response; 36 | }, 37 | responseError: function(response){ 38 | $rootScope.http.count--; 39 | if(!$rootScope.http.count){ 40 | $rootScope.http.loading = false; 41 | } 42 | return response; 43 | } 44 | }; 45 | }); -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | $main_color: rgba(33, 150, 243, 1); 2 | 3 | /** 4 | Override bootstrap styles 5 | */ 6 | .navbar-default{ 7 | border-radius: 0; 8 | .navbar-nav{ 9 | li{ 10 | &.current{ 11 | a{ 12 | color: black; 13 | } 14 | } 15 | } 16 | } 17 | } 18 | .col-sm-2{ 19 | padding:0; 20 | } 21 | 22 | body{ 23 | background:$main_color; 24 | } 25 | 26 | section{ 27 | margin-bottom:50px; 28 | .container{ 29 | min-height:1000px; 30 | border-radius: 2px; 31 | background-color: #FFFFFF; 32 | overflow: hidden; 33 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); 34 | } 35 | } 36 | 37 | .grid{ 38 | .book{ 39 | height:200px; 40 | text-align: center; 41 | margin-bottom:20px; 42 | line-height: 200px; 43 | @media screen and (max-width: 768px) { 44 | height:400px; 45 | line-height: 400px; 46 | } 47 | @media screen and (max-width: 625px) { 48 | height:300px; 49 | line-height: 300px; 50 | } 51 | img{ 52 | vertical-align: bottom; 53 | max-width:100%; 54 | max-height:100%; 55 | bordeR:1px solid black; 56 | } 57 | } 58 | } 59 | 60 | .markdown-body { 61 | min-width: 200px; 62 | margin: 0 auto; 63 | padding: 20px 0; 64 | } 65 | 66 | .search-item{ 67 | img{ 68 | bordeR:1px solid black; 69 | float:left; 70 | margin: 0 20px 0 0; 71 | } 72 | } 73 | 74 | .search-results{ 75 | min-height:880px; 76 | } 77 | 78 | .notfound{ 79 | text-align: center; 80 | img{ 81 | width:600px; 82 | max-width:100%; 83 | margin:40px auto; 84 | } 85 | } 86 | 87 | .book, .author{ 88 | .book_cover{ 89 | float:left; 90 | border:1px solid black; 91 | margin: 0 20px 20px 0; 92 | width:247px; 93 | max-width:100%; 94 | min-height:100px; 95 | @media screen and (max-width: 625px) { 96 | float:none; display:block; margin: 20px auto; 97 | } 98 | } 99 | } 100 | 101 | .authors{ 102 | color:#666; 103 | } 104 | 105 | .author-books{ 106 | min-height:440px; 107 | } 108 | 109 | .average_rating{ 110 | cursor: pointer; 111 | } 112 | 113 | .average_rating_label{ 114 | display:none; 115 | } 116 | 117 | .average_rating:hover + .average_rating_label{ 118 | display:inline-block; 119 | } 120 | 121 | .progress{ 122 | position:absolute; 123 | top:0; 124 | left:0; 125 | right:0; 126 | height:5px; 127 | .progress-bar{ 128 | -webkit-transition-duration:5s; 129 | transition-duration:5s; 130 | } 131 | &.ng-enter, &.ng-leave { 132 | -webkit-transition:opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; 133 | transition:opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; 134 | 135 | opacity:1; 136 | } 137 | 138 | &.ng-leave.ng-leave-active, &.ng-enter { 139 | opacity:0; 140 | } 141 | 142 | &.ng-leave.ng-leave-active { 143 | .progress-bar{ 144 | -webkit-transition:none; 145 | transition:none; 146 | } 147 | } 148 | 149 | &.ng-enter.ng-enter-active { 150 | opacity:1 151 | } 152 | } 153 | 154 | .view-animate-container { 155 | position:relative; 156 | } 157 | 158 | .view-animate{ 159 | &.ng-enter, &.ng-leave { 160 | -webkit-transition:opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; 161 | transition:opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; 162 | 163 | opacity:1; 164 | position:absolute; 165 | top:0; 166 | left:15px; // set from bootstrap 167 | right:15px; 168 | bottom:0; 169 | } 170 | 171 | &.ng-leave.ng-leave-active, &.ng-enter { 172 | opacity:0; 173 | } 174 | 175 | &.ng-enter.ng-enter-active { 176 | opacity:1 177 | } 178 | } 179 | 180 | .include-animate-container { 181 | position:relative; 182 | } 183 | 184 | .include-animate{ 185 | &.ng-enter, &.ng-leave { 186 | -webkit-transition:opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; 187 | transition:opacity cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.3s; 188 | 189 | opacity:1; 190 | position:absolute; 191 | top:0; 192 | left:0; 193 | right:0; 194 | bottom:0; 195 | } 196 | 197 | &.ng-leave.ng-leave-active, &.ng-enter { 198 | opacity:0; 199 | } 200 | 201 | &.ng-enter.ng-enter-active { 202 | opacity:1 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app/views/404.handlebars: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /app/views/author.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

{{name}}

3 | 4 | {{#if image}} 5 | {{name}} 6 | {{/if}} 7 | 8 | {{#if fans_count}}

Number of fans {{fans_count}}

{{/if}} 9 | 10 |

{{{about}}}

11 | 12 |
13 |

Books

14 |
15 |
16 |
17 |
-------------------------------------------------------------------------------- /app/views/book.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

{{title}}

3 | 4 | {{#if image}} 5 | {{title}} 6 | {{/if}} 7 |

{{#each authors}}{{name}}{{/each}}

8 | 9 | {{#if publisher}}

Publisher {{publisher}}

{{/if}} 10 | {{#if publicationDate}}

Publication date {{publicationDate}}

{{/if}} 11 | {{#if num_pages}}

Number of pages {{num_pages}}

{{/if}} 12 | {{#if average_rating}} 13 |

14 | Average rating 15 | {{average_rating}} 16 | 17 |

18 | {{/if}} 19 | 20 |

{{{description}}}

21 | 22 |
23 |

Similar Books

24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /app/views/home.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{{md 'README.md'}}} 3 |
-------------------------------------------------------------------------------- /app/views/search.handlebars: -------------------------------------------------------------------------------- 1 |
2 |

{{title}}

3 | {{>templates/pagination}} 4 |
5 |
6 | {{>templates/books}} 7 |
8 |
9 | {{>templates/pagination}} 10 |
11 | 12 | {{!-- 13 | 22 | --}} -------------------------------------------------------------------------------- /app/views/templates/book.handlebars: -------------------------------------------------------------------------------- 1 | {{#if image}} 2 |
3 | {{title}} 4 |
5 | {{/if}} -------------------------------------------------------------------------------- /app/views/templates/books.handlebars: -------------------------------------------------------------------------------- 1 |
2 | {{#each books}} 3 | {{>templates/book}} 4 | {{/each}} 5 |
-------------------------------------------------------------------------------- /app/views/templates/pagination.handlebars: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Magma", 3 | "version": "0.0.58", 4 | "keywords": ["architecture", "client", "server"], 5 | "ignore": ["app", "ci", "docs", "grunt", "Gruntfile.js", ".travis.yml", ".openshift", "models", "routes", "index.js", "karma.conf.js", ".jshintrc", ".bowerrc", "services", "test"], 6 | "main": ["magma.min.js"], 7 | "devDependencies": { 8 | "angular-mocks": "~1.3.11", 9 | "angular": "~1.3.11", 10 | "angular-bootstrap": "~0.12.0", 11 | "angular-route": "~1.3.11", 12 | "bootswatch": "~3.3.*", 13 | "angular-animate": "~1.3.11", 14 | "github-markdown-css": "~2.0.2" 15 | }, 16 | "resolutions": { 17 | "angular": "~1.3.11" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ci/before_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #echo "export GOOGLE_KEY=$GOOGLE_KEY" > .openshift/action_hooks/build && chmod +x .openshift/action_hooks/build 4 | git rm -r app 5 | git rm -r grunt 6 | git rm -r ci 7 | git rm .gitignore 8 | git rm Gruntfile.js 9 | git add dist 10 | #git add .openshift 11 | git commit -m "Saving artefacts" 12 | 13 | echo "Done" -------------------------------------------------------------------------------- /ci/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo 'Switching to master and setting identity for git' 4 | git checkout master 5 | git config user.name $GIT_NAME 6 | git config user.email $GIT_EMAIL 7 | git config credential.helper "store --file=.git/credentials" 8 | git config remote.origin.url https://github.com/vilmosioo/magma.git 9 | echo "https://${GITHUB_TOKEN}:@github.com" > .git/credentials 10 | 11 | echo 'Patching version...' 12 | grunt bump-only:patch 13 | 14 | echo 'Running build command' 15 | grunt -v || { echo 'Client build failed' ; exit 1; } 16 | 17 | echo 'Pushing git data to repo...' 18 | grunt bump-commit 19 | 20 | exit 0; -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/docs/architecture.png -------------------------------------------------------------------------------- /docs/client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/docs/client.png -------------------------------------------------------------------------------- /docs/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/docs/index.png -------------------------------------------------------------------------------- /docs/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vilmosioo/magma/eddd7cff5f4214975347d1941ba26c81be4d2427/docs/server.png -------------------------------------------------------------------------------- /grunt/bump.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | options: { 5 | files: ['package.json', 'bower.json'], 6 | updateConfigs: ['pck'], 7 | commit: true, 8 | commitMessage: 'Release v%VERSION% [skip ci]', 9 | commitFiles: ['package.json', 'bower.json', 'magma.min.js'], 10 | createTag: true, 11 | tagName: 'v%VERSION%', 12 | tagMessage: 'Version %VERSION%', 13 | push: true, 14 | pushTo: 'origin', 15 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d', 16 | globalReplace: false 17 | } 18 | }; -------------------------------------------------------------------------------- /grunt/clean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | server: { 5 | src: ['<%= pck.config.tmp %>'] 6 | }, 7 | dist: { 8 | src: ['<%= pck.config.dist %>', '<%= pck.config.tmp %>'] 9 | } 10 | }; -------------------------------------------------------------------------------- /grunt/copy.js: -------------------------------------------------------------------------------- 1 | 'use stricts'; 2 | 3 | module.exports = { 4 | static: { 5 | files: [{ 6 | expand: true, 7 | cwd: '<%= pck.config.app %>', 8 | src: ['**/*.{handlebars,ico,html}'], 9 | dest: '<%= pck.config.dist %>' 10 | }, { 11 | expand: true, 12 | cwd: '<%= pck.config.app %>/components/bootswatch', 13 | src: ['fonts/*.{eot,svg,ttf,woff,woff2}'], 14 | dest: '<%= pck.config.dist %>' 15 | }] 16 | }, 17 | magma: { 18 | files: { 19 | 'magma.min.js': '<%= pck.config.dist %>/scripts/magma.js' 20 | } 21 | } 22 | }; -------------------------------------------------------------------------------- /grunt/express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | options: { 5 | script: 'index.js', 6 | output: 'Server listening .+' 7 | }, 8 | server: { 9 | options:{ 10 | node_env: 'development' 11 | } 12 | }, 13 | dist: { 14 | options: { 15 | node_env: 'production', 16 | background: false 17 | } 18 | } 19 | }; -------------------------------------------------------------------------------- /grunt/filerev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | js: { 5 | src: ['<%= pck.config.dist %>/**/*.js'] 6 | }, 7 | css: { 8 | src: ['<%= pck.config.dist %>/**/*.css'] 9 | } 10 | }; -------------------------------------------------------------------------------- /grunt/imagemin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | all: { 5 | files: [{ 6 | expand: true, 7 | cwd: '<%= pck.config.app %>/images', 8 | src: ['**/*.{png,jpg,gif}'], 9 | dest: '<%= pck.config.dist %>/images' 10 | }, { 11 | expand: true, 12 | cwd: '<%= pck.config.docs %>', 13 | src: ['**/*.{png,jpg,gif}'], 14 | dest: '<%= pck.config.dist %>/<%= pck.config.docs %>' 15 | }] 16 | } 17 | }; -------------------------------------------------------------------------------- /grunt/jshint.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | options: { 5 | jshintrc: '.jshintrc' 6 | }, 7 | all: ['Gruntfile.js', '<%= pck.config.app %>/scripts/**/*.js', 'grunt/**/*.js', 'models/**/*.js', 'routes/**/*.js', 'test/**/*.js', '<%= pck.main %>'] 8 | }; -------------------------------------------------------------------------------- /grunt/karma.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | unit: { 5 | configFile: 'karma.conf.js' 6 | } 7 | }; -------------------------------------------------------------------------------- /grunt/ngAnnotate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | dist: { 5 | files: { 6 | '<%= pck.config.tmp %>/concat/scripts/app.js': ['<%= pck.config.tmp %>/concat/scripts/app.js'], 7 | '<%= pck.config.tmp %>/concat/scripts/magma.js': ['<%= pck.config.tmp %>/concat/scripts/magma.js'] 8 | } 9 | } 10 | }; -------------------------------------------------------------------------------- /grunt/open.js: -------------------------------------------------------------------------------- 1 | 'use stricts'; 2 | 3 | module.exports = { 4 | server: { 5 | path: 'http://localhost:<%= pck.config.port %>' 6 | } 7 | }; -------------------------------------------------------------------------------- /grunt/sass.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | dist: { 5 | files: [{ 6 | expand: true, 7 | cwd: '<%= pck.config.app %>/styles', 8 | src: ['**/*.scss'], 9 | dest: '<%= pck.config.tmp %>/styles', 10 | ext: '.css' 11 | }] 12 | }, 13 | server: { 14 | options: { 15 | sourceMap: true 16 | }, 17 | files: [{ 18 | expand: true, 19 | cwd: '<%= pck.config.app %>/styles', 20 | src: ['**/*.scss'], 21 | dest: '<%= pck.config.tmp %>/styles', 22 | ext: '.css' 23 | }] 24 | } 25 | }; -------------------------------------------------------------------------------- /grunt/uglify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | options:{ 5 | banner: '/*! <%= pck.name %> - v<%= pck.version %> - ' + 6 | '<%= grunt.template.today("yyyy-mm-dd") %> */' 7 | } 8 | }; -------------------------------------------------------------------------------- /grunt/usemin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | html: ['<%= pck.config.dist %>/{,*/}*.handlebars'], 5 | css: ['<%= pck.config.dist %>/{,*/}*.css'], 6 | js: ['<%= pck.config.dist %>/{,*/}*.js'], 7 | options: { 8 | assetsDirs: ['<%= pck.config.dist %>', '<%= pck.config.dist %>/images', '<%= pck.config.dist %>/styles/fonts'] 9 | } 10 | }; -------------------------------------------------------------------------------- /grunt/useminPrepare.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | html: '<%= pck.config.app %>/index.handlebars', 5 | options: { 6 | dest: '<%= pck.config.dist %>' 7 | } 8 | }; -------------------------------------------------------------------------------- /grunt/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | express: { 5 | files: [ '<%= pck.main %>', '{models,routes,services}/**/*.{js,json}'], 6 | tasks: [ 'express:server' ], 7 | options: { 8 | spawn: false, // for grunt-contrib-watch v0.5.0+, "nospawn: true" for lower versions. Without this option specified express won't be reloaded 9 | livereload: true 10 | } 11 | }, 12 | sass: { 13 | files: [ '<%= pck.config.app %>/**/*.scss'], 14 | tasks: [ 'sass:server' ], 15 | options: { 16 | livereload: true 17 | } 18 | }, 19 | js: { 20 | files: [ '<%= jshint.all %>'], 21 | tasks: [ 'jshint' ] 22 | }, 23 | livereload: { 24 | options: { 25 | livereload: true 26 | }, 27 | files: [ 28 | 'README.md', 29 | '<%= pck.config.app %>/**/*.{handlebars,html,js}', 30 | '<%= pck.config.tmp %> %>/styles/**/*.css', 31 | '<%= pck.config.app %>/images/**/*.{png,jpg,jpeg,gif,webp,svg}' 32 | ] 33 | } 34 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'), 4 | bodyParser = require('body-parser'), 5 | logger = require('morgan'), 6 | path = require('path'), 7 | exphbs = require('express-handlebars'), 8 | pck = require('./package.json'), 9 | resources = require('./routes/static'), 10 | root = require('./routes/root'), 11 | notFound = require('./models/404'), 12 | md = require('markdown').markdown, 13 | fs = require('fs'), 14 | dir = process.env.NODE_ENV === 'development' ? pck.config.app : pck.config.dist; 15 | 16 | var app = express(), 17 | hbs = exphbs.create({ 18 | defaultLayout: 'index', 19 | layoutsDir: path.join(__dirname, dir), 20 | partialsDir: path.join(__dirname, dir + '/views'), 21 | helpers: { 22 | json: function(){ 23 | var data = {}; 24 | for(var i = 0, l = arguments.length; i < l; i++){ 25 | var key = arguments[i]; 26 | data[key] = this[key]; 27 | } 28 | return JSON.stringify(data); 29 | }, 30 | md: function(file){ 31 | return md.toHTML(fs.readFileSync(file, { encoding: 'utf8' })); 32 | } 33 | } 34 | }); 35 | 36 | app.set('views', path.join(__dirname, dir + '/views')); 37 | app.engine('handlebars', hbs.engine); 38 | app.set('view engine', 'handlebars'); 39 | app.use(logger('combined')); 40 | app.use(bodyParser.urlencoded({ extended: false })); 41 | app.use(bodyParser.json()); 42 | 43 | app.use(resources); 44 | app.use(root); 45 | app.use(function(req, res){ 46 | notFound().then(function(data){ 47 | res.render('404', data); 48 | }); 49 | }); 50 | 51 | var server = app.listen(process.env.OPENSHIFT_NODEJS_PORT || pck.config.port, process.env.OPENSHIFT_NODEJS_IP || '127.0.0.1', function(){ 52 | console.log('Server listening on ' + server.address().address + ':' + server.address().port); 53 | }); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | var fs= require('fs'), 5 | pck = JSON.parse(fs.readFileSync('./package.json').toString()); 6 | 7 | module.exports = function(config) { 8 | config.set({ 9 | // base path, that will be used to resolve files and exclude 10 | basePath: '', 11 | 12 | // testing framework to use (jasmine/mocha/qunit/...) 13 | frameworks: ['jasmine'], 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | // jasmine matchers 18 | 'node_modules/jasmine-expect/dist/jasmine-matchers.js', 19 | 20 | // libraries 21 | pck.config.app + '/components/angular/angular.js', 22 | pck.config.app + '/components/angular-animate/angular-animate.js', 23 | pck.config.app + '/components/angular-route/angular-route.js', 24 | pck.config.app + '/components/angular-mocks/angular-mocks.js', 25 | pck.config.app + '/components/angular-bootstrap/ui-bootstrap.js', 26 | pck.config.app + '/components/angular-bootstrap/ui-bootstrap-tpls.js', 27 | 28 | // the app 29 | pck.config.app + '/magma/**/*.js', 30 | pck.config.app + '/scripts/**/*.js', 31 | 32 | // the tests 33 | pck.config.test + '/spec/**/*.js' 34 | ], 35 | 36 | // test results reporter to use 37 | // possible values: dots || progress || growl 38 | 39 | reporters: ['progress', 'coverage'], 40 | 41 | preprocessors: { 42 | // source files, that you wanna generate coverage for 43 | // do not include tests or libraries 44 | // (these files will be instrumented by Istanbul) 45 | 'app/scripts/**/*.js': ['coverage'] 46 | }, 47 | 48 | // optionally, configure the reporter 49 | coverageReporter: { 50 | type : 'html', 51 | dir : 'coverage/' 52 | }, 53 | 54 | // list of files / patterns to exclude 55 | exclude: [], 56 | 57 | // web server port 58 | port: 8080, 59 | 60 | // level of logging 61 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 62 | logLevel: config.LOG_INFO, 63 | 64 | // enable / disable watching file and executing tests whenever any file changes 65 | autoWatch: false, 66 | 67 | // Start these browsers, currently available: 68 | // - Chrome 69 | // - ChromeCanary 70 | // - Firefox 71 | // - Opera 72 | // - Safari (only Mac) 73 | // - PhantomJS 74 | // - IE (only Windows) 75 | browsers: ['PhantomJS'], 76 | 77 | // Continuous Integration mode 78 | // if true, it capture browsers, run tests and exit 79 | singleRun: true 80 | }); 81 | }; -------------------------------------------------------------------------------- /magma.min.js: -------------------------------------------------------------------------------- 1 | /*! Magma - v0.0.58 - 2015-02-02 */"use strict";angular.module("Magma",[]),angular.module("Magma").directive("mgView",["$animate","$rootScope","$compile",function(a,b,c){return{restrict:"A",replace:!1,priority:1e3,link:function(b,d){var e,f=b.$on("$routeChangeSuccess",function(){if(e){var g=d.clone().empty();g.removeAttr("mg-view"),g.attr("ng-view",""),d.after(g),a.leave(d),c(g)(b),f()}e=!0})}}}]),angular.module("Magma").directive("mgBind",["$rootScope","$compile",function(a,b){return{restrict:"A",priority:1e3,link:function(a,c,d){var e=a.$watch(function(){return a.$eval(d.mgBind)},function(f){f&&(c.attr("ng-bind",d.mgBind),c.removeAttr("mg-bind"),b(c)(a),e())})}}}]),angular.module("Magma").directive("mgInclude",["$compile","$animate",function(a,b){return{restrict:"A",link:function(c,d,e){var f=c.$watch(function(){return c.$eval(e.mgInclude)},function(g){if(g){var h=d.clone().empty();h.attr("ng-include",e.mgInclude),h.removeAttr("mg-include"),d.after(h),b.leave(d),a(h)(c),f()}})}}}]),angular.module("Magma").directive("mgSubmit",["$compile",function(a){return{restrict:"A",replace:!1,terminal:!0,priority:1e3,link:function(b,c,d){c.removeAttr("action"),c.removeAttr("method"),c.attr("ng-submit",d.mgSubmit),c.removeAttr("mg-submit"),a(c)(b)}}}]),angular.module("Magma").directive("mgScope",function(){return{restrict:"A",link:function(a,b,c){try{angular.extend(a,JSON.parse(c.mgScope))}catch(d){console.error("mgScope requires valid JSON to parse")}}}}); -------------------------------------------------------------------------------- /models/404.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var routes = require('./ROUTES.json'), 4 | Pr = require('bluebird'); 5 | 6 | module.exports = function(){ 7 | return new Pr(function(resolve){ 8 | resolve({ 9 | constants: { 10 | ROUTES: JSON.stringify(routes) 11 | }, 12 | production: process.env.NODE_ENV !== 'development', 13 | app: { 14 | title: 'Not found' 15 | } 16 | }); 17 | }); 18 | }; -------------------------------------------------------------------------------- /models/ROUTES.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": { 3 | "templateUrl": "/views/home.html", 4 | "title": "Magma", 5 | "model": "home", 6 | "description": "A new way of building isomorphic apps." 7 | }, 8 | "/search/": { 9 | "templateUrl": "/views/search.html", 10 | "model": "search", 11 | "title": "Search results for %s", 12 | "description": "Search results", 13 | "reloadOnSearch": false 14 | }, 15 | "/book/:id": { 16 | "templateUrl": "/views/book.html", 17 | "model": "book", 18 | "title": "%s", 19 | "description": "%s" 20 | }, 21 | "/author/:id": { 22 | "templateUrl": "/views/author.html", 23 | "model": "author", 24 | "title": "%s", 25 | "description": "%s" 26 | } 27 | } -------------------------------------------------------------------------------- /models/author.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var goodreads = require('../services/goodreads'), 4 | util = require('util'), 5 | route = require('./ROUTES.json')['/author/:id']; 6 | 7 | module.exports = function(args){ 8 | 9 | var data = { 10 | title: '[Author not found]', 11 | app: { 12 | title: '[Author not found]' 13 | } 14 | }; 15 | 16 | return goodreads.author(args.params.id || args.query.id).then(function(author){ 17 | data = author; 18 | 19 | // override route metadata 20 | data.app = { 21 | title: util.format(route.title, author.name), 22 | description: author.about.replace(/(<([^>]+)>)/ig,"").substring(0, 200), 23 | image: author.image 24 | }; 25 | 26 | return data; 27 | }); 28 | }; -------------------------------------------------------------------------------- /models/book.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var books = require('../services/goodreads'); 4 | 5 | module.exports = function(args){ 6 | 7 | var data = { 8 | title: '[Book not found]', 9 | app: { 10 | title: '[Book not found]' 11 | } 12 | }; 13 | 14 | return books.get(args.params.id || args.query.id).then(function(book){ 15 | data = book; 16 | 17 | // override route metadata 18 | data.app = { 19 | title: book.title, 20 | description: book.description.replace(/(<([^>]+)>)/ig,"").substring(0, 200), 21 | image: book.image 22 | }; 23 | return data; 24 | }); 25 | }; -------------------------------------------------------------------------------- /models/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var route = require('./ROUTES.json')['/'], 4 | Pr = require('bluebird'); 5 | 6 | module.exports = function(){ 7 | return new Pr(function(resolve){ 8 | resolve({ 9 | title: route.title, 10 | content: route.description 11 | }); 12 | }); 13 | }; -------------------------------------------------------------------------------- /models/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var route = require('./ROUTES.json')['/search/'], 4 | books = require('./templates/books'), 5 | extend = require('extend'), 6 | util = require('util'); 7 | 8 | module.exports = function(args){ 9 | 10 | var data = { 11 | title: util.format(route.title, args.query.q), 12 | books: [], 13 | app: { 14 | title: util.format(route.title, args.query.q), 15 | description: util.format(route.title, args.query.q) 16 | } 17 | }, _defaults = { 18 | offset: 0, 19 | limit: 999 20 | }; 21 | 22 | extend(args.query, _defaults); 23 | 24 | 25 | return books(args).then(function(view){ 26 | return extend(data, view); 27 | }); 28 | }; -------------------------------------------------------------------------------- /models/templates/books.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Pr = require('bluebird'), 4 | goodreads = require('../../services/goodreads'), 5 | querystring = require('querystring'); 6 | 7 | module.exports = function(args){ 8 | return new Pr(function(resolve, reject){ 9 | if(args.query.q){ 10 | goodreads.search(querystring.escape(args.query.q), args.query).then(function(books){ 11 | resolve({ 12 | books: books.items, 13 | pagination: books.pagination 14 | }); 15 | }, reject); 16 | } else if(args.query.author){ 17 | goodreads.booksByAuthor(querystring.escape(args.query.author), args.query).then(function(books){ 18 | resolve({ 19 | books: books 20 | }); 21 | }, reject); 22 | } else if(args.query.similar){ 23 | goodreads.similarBooks(querystring.escape(args.query.similar), args.query).then(function(books){ 24 | resolve({ 25 | books: books 26 | }); 27 | }, reject); 28 | } else { 29 | reject('I don\'t know what books you\'re looking for'); 30 | } 31 | 32 | }); 33 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Magma", 3 | "version": "0.0.58", 4 | "description": "A new way of building isomorphic app.", 5 | "keywords": [], 6 | "author": { 7 | "name": "Vilmos Ioo", 8 | "url": "vilmosioo.co.uk" 9 | }, 10 | "scripts": { 11 | "build": "sh ci/build.sh", 12 | "before_deploy": "sh ci/before_deploy.sh" 13 | }, 14 | "config": { 15 | "app": "app", 16 | "dist": "dist", 17 | "tmp": ".tmp", 18 | "test": "test", 19 | "docs": "docs", 20 | "port": 9000 21 | }, 22 | "homepage": "http://magma-vilmosioo.rhcloud.com/", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/vilmosioo/magma" 26 | }, 27 | "engines": { 28 | "node": ">= 0.6.0", 29 | "npm": ">= 1.0.0" 30 | }, 31 | "dependencies": { 32 | "morgan": "1.5.1", 33 | "body-parser": "1.10.1", 34 | "express": "4.10.7", 35 | "express-handlebars": "1.1.0", 36 | "bluebird": "2.7.0", 37 | "request-promise": "^0.3.2", 38 | "xml2js": "^0.4.4", 39 | "extend": "^2.0.0", 40 | "markdown": "^0.5.0" 41 | }, 42 | "devDependencies": { 43 | "connect-livereload": "0.5.2", 44 | "grunt-express-server": "0.4.19", 45 | "grunt-contrib-copy": "0.7.0", 46 | "matchdep": "0.3.0", 47 | "grunt": "0.4.5", 48 | "grunt-contrib-htmlmin": "0.3.0", 49 | "grunt-contrib-watch": "0.6.1", 50 | "grunt-contrib-clean": "0.6.0", 51 | "grunt-open": "0.2.3", 52 | "grunt-usemin": "3.0.0", 53 | "grunt-filerev": "2.1.2", 54 | "grunt-contrib-concat": "0.5.0", 55 | "grunt-contrib-uglify": "0.7.0", 56 | "grunt-contrib-cssmin": "0.11.0", 57 | "grunt-sass": "0.17.0", 58 | "grunt-ng-annotate": "0.8.0", 59 | "grunt-contrib-jshint": "0.10.0", 60 | "grunt-contrib-imagemin": "^0.9.2", 61 | "grunt-bump": "0.0.16", 62 | "karma": "^0.12.31", 63 | "grunt-karma": "^0.10.1", 64 | "karma-coverage": "^0.2.7", 65 | "jasmine-core": "^2.1.3", 66 | "karma-jasmine": "^0.3.5", 67 | "karma-phantomjs-launcher": "^0.1.4", 68 | "jasmine-expect": "^1.22.3" 69 | }, 70 | "bundleDependencies": [], 71 | "private": true, 72 | "main": "index.js" 73 | } 74 | -------------------------------------------------------------------------------- /routes/root.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express= require('express'), 4 | path = require('path'), 5 | routes = require('../models/ROUTES.json'), 6 | Pr = require('bluebird'), 7 | extend = require('extend'), 8 | notFound = require('../models/404'), 9 | router = express.Router(); 10 | 11 | var _render = function(route, isTemplate){ 12 | var data = !isTemplate ? { 13 | constants: { 14 | ROUTES: JSON.stringify(routes) 15 | }, 16 | production: process.env.NODE_ENV !== 'development' 17 | } : { 18 | layout: false 19 | }; 20 | 21 | if(route){ 22 | data.app = { 23 | title: route.title, 24 | description: route.description 25 | }; 26 | } 27 | 28 | return function(req, res){ 29 | var name = !route ? path.join(req.params.path || '', req.params.filename) : path.basename(route.templateUrl, path.extname(route.templateUrl)), model; 30 | 31 | // the model for the view might not be defined 32 | try{ 33 | model = require(path.join('../models/', name)); 34 | } catch(e){ 35 | model = function(){ 36 | return Pr.resolve(); 37 | }; 38 | } 39 | 40 | model({query: req.query || {}, params: req.params || {}}).then(function(view){ 41 | data = extend(data, view); 42 | 43 | if(route){ 44 | data.app.url = req.protocol + '://' + req.get('host') + req.originalUrl; 45 | } 46 | 47 | Object.keys(data.app || {}).forEach(function(key){ 48 | res.set('x-app-' + key, data.app[key]); 49 | }); 50 | 51 | res.render(name, data); 52 | }, function(err){ 53 | console.log(err); 54 | if(err.statusCode === 404){ 55 | notFound().then(function(data){ 56 | res.render('404', data); 57 | }); 58 | } else { 59 | res.status(500).send(''); 60 | } 61 | }); 62 | }; 63 | }; 64 | 65 | for(var key in routes){ 66 | if(routes.hasOwnProperty(key)){ 67 | var route = routes[key]; 68 | 69 | router.get(key, _render(route)); 70 | router.get(route.templateUrl, _render(route, true)); 71 | } 72 | } 73 | 74 | router.get('/views/:path?/:filename.html', _render(null, true)); 75 | 76 | module.exports = router; -------------------------------------------------------------------------------- /routes/static.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express= require('express'), 4 | path = require('path'), 5 | pck = require('../package.json'), 6 | router = express.Router(); 7 | 8 | if(process.env.NODE_ENV === 'development'){ 9 | router.use(require('connect-livereload')()); 10 | router.use(express.static(path.join(__dirname, '..', pck.config.app))); 11 | router.use(express.static(path.join(__dirname, '..', pck.config.tmp))); 12 | router.use(express.static(path.join(__dirname, '..'))); // this is only required for sourcemaps 13 | } else { 14 | // express will not actually serve any static files, this is just a fallback, nginx will take care of this 15 | router.use(express.static(path.join(__dirname, '..', pck.config.dist))); 16 | } 17 | 18 | module.exports = router; -------------------------------------------------------------------------------- /services/goodreads.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('request-promise'), 4 | util = require('util'), 5 | Pr = require('bluebird'), 6 | xml2js = require('xml2js'), 7 | parser = Pr.promisify((new xml2js.Parser()).parseString), 8 | extend = require('extend'), 9 | SEARCH = 'https://www.goodreads.com/search/index.xml?q=%s&page=%s&key=' + process.env.GOODREADS_KEY, 10 | GET = 'https://www.goodreads.com/book/show/%s?key=' + process.env.GOODREADS_KEY, 11 | AUTHOR = 'https://www.goodreads.com/author/show/%s.xml?key=' + process.env.GOODREADS_KEY, 12 | BOOKS_BY_AUTHOR = 'https://www.goodreads.com/author/list/%s.xml?key=' + process.env.GOODREADS_KEY; 13 | 14 | var _formatBook = function(book){ 15 | var obj = ['id', 'isbn', 'title', 'isbn13', 'description', 'publisher', 'average_rating', 'num_pages'].reduce(function(obj, current){ 16 | obj[current] = book[current][0]; 17 | return obj; 18 | }, {}); 19 | obj.publicationDate = new Date(book.publication_year[0], book.publication_month[0], book.publication_day[0]); 20 | obj.image = book.image_url[0].replace(/(\d+)[m,s]\//, '$1l/'); 21 | obj.authors = book.authors.map(function(item){ 22 | var author = item.author[0]; 23 | return ['name', 'image_url', 'average_rating', 'id'].reduce(function(o, current){ 24 | o[current] = author[current][0]; 25 | return o; 26 | }, {}) 27 | }); 28 | return obj; 29 | }; 30 | 31 | var _formatBookLite = function(item){ 32 | return { 33 | id: item.id[0]._ || item.id[0], 34 | title: item.title[0], 35 | image: item.image_url[0].replace(/(\d+)[m,s]\//, '$1l/') 36 | } 37 | }; 38 | 39 | var _defaults = { 40 | offset: 0, 41 | limit: 12, 42 | page: 1 43 | }; 44 | 45 | module.exports = { 46 | similarBooks: function(id, options){ 47 | options = extend({}, _defaults, options); 48 | 49 | if(!!id){ 50 | console.log('Request => ' + util.format(GET, id)); 51 | return request(util.format(GET, id), { 52 | rejectUnauthorized: false 53 | }) 54 | .then(function(response){ 55 | return parser(response); 56 | }) 57 | .then(function(response){ 58 | return response.GoodreadsResponse.book[0]; 59 | }) 60 | .then(function(book){ 61 | return book.similar_books[0].book; 62 | }) 63 | .then(function(books){ 64 | return books.slice(options.offset, options.limit).map(_formatBookLite); 65 | }); 66 | } else { 67 | return new Pr(function(resolve, reject){ 68 | reject({ 69 | error: 'ID must be specified' 70 | }); 71 | }); 72 | } 73 | }, 74 | booksByAuthor: function(id, options){ 75 | options = extend({}, _defaults, options); 76 | if(!!id){ 77 | console.log('Request => ' + util.format(BOOKS_BY_AUTHOR, id)); 78 | return request(util.format(BOOKS_BY_AUTHOR, id), { 79 | rejectUnauthorized: false 80 | }) 81 | .then(function(response){ 82 | return parser(response); 83 | }) 84 | .then(function(response){ 85 | return response.GoodreadsResponse.author[0]; 86 | }) 87 | .then(function(author){ 88 | return author.books[0].book.slice(options.offset, options.limit).map(_formatBookLite); 89 | }); 90 | } else { 91 | return new Pr(function(resolve, reject){ 92 | reject({ 93 | error: 'ID must be specified' 94 | }); 95 | }); 96 | } 97 | }, 98 | author: function(id, options){ 99 | options = extend({}, _defaults, options); 100 | if(!!id){ 101 | console.log('Request => ' + util.format(AUTHOR, id)); 102 | return request(util.format(AUTHOR, id), { 103 | rejectUnauthorized: false 104 | }) 105 | .then(function(response){ 106 | return parser(response); 107 | }) 108 | .then(function(response){ 109 | return response.GoodreadsResponse.author[0]; 110 | }) 111 | .then(function(author){ 112 | var obj = ['id', 'name', 'about'].reduce(function(obj, current){ 113 | obj[current] = author[current][0]; 114 | return obj; 115 | }, {}); 116 | obj.fans_count = author.fans_count[0]._; 117 | obj.image = author.image_url[0]; 118 | obj.books = author.books[0].book.slice(options.offset, options.limit).map(_formatBook); 119 | return obj; 120 | }); 121 | } else { 122 | return new Pr(function(resolve, reject){ 123 | reject({ 124 | error: 'ID must be specified' 125 | }); 126 | }); 127 | } 128 | }, 129 | get: function(id){ 130 | if(!!id){ 131 | console.log('Request => ' + util.format(GET, id)); 132 | return request(util.format(GET, id), { 133 | rejectUnauthorized: false 134 | }) 135 | .then(function(response){ 136 | return parser(response); 137 | }) 138 | .then(function(response){ 139 | return response.GoodreadsResponse.book[0]; 140 | }) 141 | .then(_formatBook); 142 | } else { 143 | return new Pr(function(resolve, reject){ 144 | reject({ 145 | error: 'ID must be specified' 146 | }); 147 | }); 148 | } 149 | }, 150 | search: function(q, options){ 151 | options = extend({}, _defaults, options); 152 | if(!!q){ 153 | console.log('Request => ' + util.format(SEARCH, q, options.page)); 154 | return request(util.format(SEARCH, q, options.page), { 155 | rejectUnauthorized: false 156 | }) 157 | .then(function(response){ 158 | return parser(response); 159 | }) 160 | .then(function(response){ 161 | var search = response.GoodreadsResponse.search[0]; 162 | return { 163 | items: search.results[0].work, 164 | pagination: { 165 | current: options.page || 1, 166 | perPage: search.results[0].work.length, 167 | total: parseInt(search['total-results'][0], 10) 168 | } 169 | } 170 | }) 171 | .then(function(data){ 172 | data.items = data.items.slice(options.offset, options.limit).map(function(work){ 173 | return work.best_book[0]; 174 | }); 175 | return data; 176 | }) 177 | .then(function(data){ 178 | data.items = data.items.map(_formatBookLite); 179 | return data; 180 | }); 181 | } else { 182 | return new Pr(function(resolve, reject){ 183 | reject({ 184 | error: 'Query must be specified' 185 | }); 186 | }); 187 | } 188 | } 189 | }; -------------------------------------------------------------------------------- /test/spec/directives/mgBind_directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('mgBind spec', function(){ 4 | var $compile, $rootScope, values = { 5 | original: 'Original content', 6 | dynamic: 'Dynamic content' 7 | }; 8 | 9 | beforeEach(module('Magma')); 10 | beforeEach(inject(function(_$compile_, _$rootScope_){ 11 | $compile = _$compile_; 12 | $rootScope = _$rootScope_; 13 | })); 14 | 15 | it('should keep content if value is falsy', function(){ 16 | var $element = $compile('
'+values.original+'
')($rootScope); 17 | $rootScope.$digest(); 18 | 19 | expect($element.text()).toEqual(values.original); 20 | expect($element[0].hasAttribute('mg-bind')).toBe(true); 21 | }); 22 | 23 | it('should replace itself with ngBind if value is truthy', function(){ 24 | var scope = $rootScope.$new(), $element; 25 | scope.defined = values.dynamic; 26 | 27 | $element = $compile('
'+values.original+'
')(scope); 28 | $rootScope.$digest(); 29 | 30 | expect($element.text()).toEqual(values.dynamic); 31 | expect($element[0].hasAttribute('mg-bind')).toBe(false); 32 | 33 | $element = $compile('
'+values.original+'
')(scope); 34 | $rootScope.$digest(); 35 | 36 | expect($element.text()).toEqual(values.dynamic); 37 | expect($element.attr('ng-bind')).toBe('\'' + values.dynamic + '\''); 38 | expect($element[0].hasAttribute('mg-bind')).toBe(false); 39 | }); 40 | }); -------------------------------------------------------------------------------- /test/spec/directives/mgInclude_directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('mgInclude spec', function(){ 4 | var $compile, 5 | $httpBackend, 6 | $rootScope, 7 | $animate, 8 | $q, 9 | original = 'Original content', 10 | url = 'dynamic.html', 11 | response = 'Dynamic content'; 12 | 13 | beforeEach(module('Magma')); 14 | beforeEach(inject(function(_$compile_, _$rootScope_, _$httpBackend_, _$animate_, _$q_){ 15 | $compile = _$compile_; 16 | $rootScope = _$rootScope_; 17 | $httpBackend = _$httpBackend_; 18 | $animate = _$animate_; 19 | $q = _$q_; 20 | })); 21 | 22 | afterEach(function() { 23 | $httpBackend.verifyNoOutstandingExpectation(); 24 | $httpBackend.verifyNoOutstandingRequest(); 25 | }); 26 | 27 | it('should keep content if value is falsy', function(){ 28 | var $element = $compile('
'+original+'
')($rootScope); 29 | $rootScope.$digest(); 30 | 31 | expect($element.text()).toEqual(original); 32 | expect($element[0].hasAttribute('mg-include')).toBe(true); 33 | }); 34 | 35 | it('should replace itself with ngInclude if value is truthy', function(){ 36 | var scope = $rootScope.$new(), $element; 37 | scope.defined = url; 38 | 39 | $element = $compile('
'+original+'
')(scope); 40 | expect($element.next().length).toBe(0); 41 | $httpBackend.expectGET(url).respond(200, response); 42 | spyOn($animate, 'leave'); 43 | $httpBackend.flush(); 44 | $rootScope.$digest(); 45 | 46 | expect($animate.leave).toHaveBeenCalledWith($element); 47 | expect($element.next().text()).toEqual(response); 48 | expect($element.next()[0].hasAttribute('ng-include')).toBe(true); 49 | }); 50 | }); -------------------------------------------------------------------------------- /test/spec/directives/mgScope_directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('mgScope spec', function(){ 4 | var $compile, $rootScope, value = { 5 | test: 1 6 | }; 7 | 8 | beforeEach(module('Magma')); 9 | beforeEach(inject(function(_$compile_, _$rootScope_){ 10 | $compile = _$compile_; 11 | $rootScope = _$rootScope_; 12 | })); 13 | 14 | it('should expand scope with JSON object', function(){ 15 | var scope = $rootScope.$new(); 16 | 17 | $compile('
')(scope); 18 | $rootScope.$digest(); 19 | 20 | expect(scope).toImplement(value); 21 | }); 22 | 23 | it('should throw error if mgScope contains an invalid value', function(){ 24 | spyOn(console, 'error'); 25 | $compile('
')($rootScope.$new()); 26 | $rootScope.$digest(); 27 | 28 | expect(console.error).toHaveBeenCalledWith('mgScope requires valid JSON to parse'); 29 | }); 30 | }); -------------------------------------------------------------------------------- /test/spec/directives/mgSubmit_directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('mgSubmit spec', function(){ 4 | var $compile, $rootScope; 5 | 6 | beforeEach(module('Magma')); 7 | beforeEach(inject(function(_$compile_, _$rootScope_){ 8 | $compile = _$compile_; 9 | $rootScope = _$rootScope_; 10 | })); 11 | 12 | it('should remove action and method attributes from form elements', function(){ 13 | var onsubmit = 'onsubmit()', $element = $compile('
')($rootScope); 14 | $rootScope.$digest(); 15 | 16 | expect($element[0].hasAttribute('method')).toBe(false); 17 | expect($element[0].hasAttribute('action')).toBe(false); 18 | expect($element.attr('ng-submit')).toBe(onsubmit); 19 | }); 20 | }); -------------------------------------------------------------------------------- /test/spec/directives/mgView_directive.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('mgView spec', function(){ 4 | var $compile, 5 | original = 'original content', 6 | $animate, 7 | $rootScope; 8 | 9 | beforeEach(module('Magma')); 10 | beforeEach(inject(function(_$compile_, _$rootScope_, _$animate_){ 11 | $compile = _$compile_; 12 | $rootScope = _$rootScope_; 13 | $animate = _$animate_; 14 | })); 15 | 16 | it('should keep content if value is falsy', function(){ 17 | var $element = $compile('
'+original+'
')($rootScope); 18 | $rootScope.$digest(); 19 | 20 | expect($element.text()).toEqual(original); 21 | expect($element[0].hasAttribute('mg-view')).toBe(true); 22 | }); 23 | 24 | it('should replace itself with ngView on routeChangeSuccess', function(){ 25 | var $element = $compile('
'+original+'
')($rootScope); 26 | 27 | expect($element.next().length).toBe(0); 28 | spyOn($animate, 'leave'); 29 | $rootScope.$broadcast('$routeChangeSuccess'); // should ignore first event 30 | $rootScope.$broadcast('$routeChangeSuccess'); 31 | 32 | expect($animate.leave).toHaveBeenCalledWith($element); 33 | expect($element.next()[0].hasAttribute('ng-view')).toBe(true); 34 | }); 35 | }); --------------------------------------------------------------------------------