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/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Magma [](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 | 
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 | 
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 | 
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 | 
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 |
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/).
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------