├── .bowerrc ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .npmignore ├── CONTRIBUTING.md ├── Gruntfile.js ├── README.md ├── bower.json ├── client ├── lbclient │ ├── .gitignore │ ├── boot │ │ └── replication.js │ ├── build.js │ ├── datasources.json │ ├── datasources.local.js │ ├── lbclient.js │ ├── model-config.json │ ├── models │ │ └── local-todo.json │ └── package.json ├── ngapp │ ├── .buildignore │ ├── config │ │ ├── bundle.js │ │ └── routes.json │ ├── favicon.ico │ ├── index.html │ ├── robots.txt │ ├── scripts │ │ ├── app.js │ │ ├── controllers │ │ │ ├── change.js │ │ │ ├── home.js │ │ │ ├── login.js │ │ │ ├── register.js │ │ │ ├── todo.js │ │ │ └── user.js │ │ └── services │ │ │ └── lbclient.js │ ├── styles │ │ └── main.css │ ├── test │ │ ├── .jshintrc │ │ ├── karma.conf.js │ │ └── spec │ │ │ ├── controllers │ │ │ ├── change.js │ │ │ ├── home.js │ │ │ ├── login.js │ │ │ ├── register.js │ │ │ ├── todo.js │ │ │ └── user.js │ │ │ └── services │ │ │ └── lbclient.js │ └── views │ │ ├── changes.html │ │ ├── login.html │ │ ├── register.html │ │ ├── todos.html │ │ ├── user.html │ │ └── welcome.html └── reapp │ ├── .buildignore │ ├── config │ ├── bundle.js │ └── routes.json │ ├── favicon.ico │ ├── index.html │ ├── robots.txt │ ├── scripts │ ├── actions.js │ ├── app.jsx │ ├── services │ │ └── lbclient.js │ └── stores │ │ ├── change-store.js │ │ └── todo-store.js │ ├── styles │ └── main.css │ ├── test │ ├── .jshintrc │ ├── karma.conf.js │ └── spec │ │ ├── controllers │ │ ├── change.js │ │ ├── home.js │ │ ├── login.js │ │ ├── register.js │ │ ├── todo.js │ │ └── user.js │ │ └── services │ │ └── lbclient.js │ └── views │ ├── changes.jsx │ ├── login.jsx │ ├── register.jsx │ ├── todos.jsx │ ├── user.jsx │ └── welcome.jsx ├── common └── models │ ├── test │ ├── todo.test.js │ └── user.test.js │ ├── todo.js │ ├── todo.json │ └── user.json ├── global-config.js ├── package.json └── server ├── boot ├── angular-routes.js ├── authentication.js ├── change-tracking.js ├── dev-assets.js ├── explorer.js └── rest-api.js ├── config.json ├── config.local.js ├── datasources.json ├── datasources.production.js ├── datasources.staging.js ├── model-config.json ├── package.json └── server.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.gz 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | .DS_Store 9 | .tmp 10 | build 11 | global.config.js 12 | bower_components 13 | client/dist 14 | ngapp/config/bundle.js 15 | html5/build 16 | lib-cov 17 | local.config.js 18 | logs 19 | node_modules 20 | npm-debug.log 21 | pids 22 | public/bundle.js 23 | results 24 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": "nofunc", 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "globals": { 21 | "angular": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.gz 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | .DS_Store 9 | lib-cov 10 | logs 11 | npm-debug.log 12 | pids 13 | results 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `loopback-example-full-stack`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `loopback-example-full-stack` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-example-full-stack) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Contributor License Agreement ### 23 | 24 | ``` 25 | Individual Contributor License Agreement 26 | 27 | By signing this Individual Contributor License Agreement 28 | ("Agreement"), and making a Contribution (as defined below) to 29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and 30 | agree to the following terms and conditions for Your present and 31 | future Contributions submitted to StrongLoop. Except for the license 32 | granted in this Agreement to StrongLoop and recipients of software 33 | distributed by StrongLoop, You reserve all right, title, and interest 34 | in and to Your Contributions. 35 | 36 | 1. Definitions 37 | 38 | "You" or "Your" shall mean the copyright owner or the individual 39 | authorized by the copyright owner that is entering into this 40 | Agreement with StrongLoop. 41 | 42 | "Contribution" shall mean any original work of authorship, 43 | including any modifications or additions to an existing work, that 44 | is intentionally submitted by You to StrongLoop for inclusion in, 45 | or documentation of, any of the products owned or managed by 46 | StrongLoop ("Work"). For purposes of this definition, "submitted" 47 | means any form of electronic, verbal, or written communication 48 | sent to StrongLoop or its representatives, including but not 49 | limited to communication or electronic mailing lists, source code 50 | control systems, and issue tracking systems that are managed by, 51 | or on behalf of, StrongLoop for the purpose of discussing and 52 | improving the Work, but excluding communication that is 53 | conspicuously marked or otherwise designated in writing by You as 54 | "Not a Contribution." 55 | 56 | 2. You Grant a Copyright License to StrongLoop 57 | 58 | Subject to the terms and conditions of this Agreement, You hereby 59 | grant to StrongLoop and recipients of software distributed by 60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, 61 | royalty-free, irrevocable copyright license to reproduce, prepare 62 | derivative works of, publicly display, publicly perform, 63 | sublicense, and distribute Your Contributions and such derivative 64 | works under any license and without any restrictions. 65 | 66 | 3. You Grant a Patent License to StrongLoop 67 | 68 | Subject to the terms and conditions of this Agreement, You hereby 69 | grant to StrongLoop and to recipients of software distributed by 70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable (except as stated in this Section) 72 | patent license to make, have made, use, offer to sell, sell, 73 | import, and otherwise transfer the Work under any license and 74 | without any restrictions. The patent license You grant to 75 | StrongLoop under this Section applies only to those patent claims 76 | licensable by You that are necessarily infringed by Your 77 | Contributions(s) alone or by combination of Your Contributions(s) 78 | with the Work to which such Contribution(s) was submitted. If any 79 | entity institutes a patent litigation against You or any other 80 | entity (including a cross-claim or counterclaim in a lawsuit) 81 | alleging that Your Contribution, or the Work to which You have 82 | contributed, constitutes direct or contributory patent 83 | infringement, any patent licenses granted to that entity under 84 | this Agreement for that Contribution or Work shall terminate as 85 | of the date such litigation is filed. 86 | 87 | 4. You Have the Right to Grant Licenses to StrongLoop 88 | 89 | You represent that You are legally entitled to grant the licenses 90 | in this Agreement. 91 | 92 | If Your employer(s) has rights to intellectual property that You 93 | create, You represent that You have received permission to make 94 | the Contributions on behalf of that employer, that Your employer 95 | has waived such rights for Your Contributions, or that Your 96 | employer has executed a separate Corporate Contributor License 97 | Agreement with StrongLoop. 98 | 99 | 5. The Contributions Are Your Original Work 100 | 101 | You represent that each of Your Contributions are Your original 102 | works of authorship (see Section 8 (Submissions on Behalf of 103 | Others) for submission on behalf of others). You represent that to 104 | Your knowledge, no other person claims, or has the right to claim, 105 | any right in any intellectual property right related to Your 106 | Contributions. 107 | 108 | You also represent that You are not legally obligated, whether by 109 | entering into an agreement or otherwise, in any way that conflicts 110 | with the terms of this Agreement. 111 | 112 | You represent that Your Contribution submissions include complete 113 | details of any third-party license or other restriction (including, 114 | but not limited to, related patents and trademarks) of which You 115 | are personally aware and which are associated with any part of 116 | Your Contributions. 117 | 118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions 119 | 120 | You are not expected to provide support for Your Contributions, 121 | except to the extent You desire to provide support. You may provide 122 | support for free, for a fee, or not at all. 123 | 124 | 6. No Warranties or Conditions 125 | 126 | StrongLoop acknowledges that unless required by applicable law or 127 | agreed to in writing, You provide Your Contributions on an "AS IS" 128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES 130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR 131 | FITNESS FOR A PARTICULAR PURPOSE. 132 | 133 | 7. Submission on Behalf of Others 134 | 135 | If You wish to submit work that is not Your original creation, You 136 | may submit it to StrongLoop separately from any Contribution, 137 | identifying the complete details of its source and of any license 138 | or other restriction (including, but not limited to, related 139 | patents, trademarks, and license agreements) of which You are 140 | personally aware, and conspicuously marking the work as 141 | "Submitted on Behalf of a Third-Party: [named here]". 142 | 143 | 8. Agree to Notify of Change of Circumstances 144 | 145 | You agree to notify StrongLoop of any facts or circumstances of 146 | which You become aware that would make these representations 147 | inaccurate in any respect. Email us at callback@strongloop.com. 148 | ``` 149 | 150 | [Google C++ Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml 151 | [Google Javascript Style Guide]: https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml 152 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2014-06-23 using generator-angular 0.9.1 2 | 'use strict'; 3 | 4 | var buildClientBundle = require('./client/lbclient/build'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | // # Globbing 9 | // for performance reasons we're only matching one level down: 10 | // 'test/spec/{,*/}*.js' 11 | // use this if you want to recursively match all subfolders: 12 | // 'test/spec/**/*.js' 13 | 14 | module.exports = function (grunt) { 15 | 16 | // Load grunt tasks automatically 17 | require('load-grunt-tasks')(grunt); 18 | 19 | // Time how long tasks take. Can help when optimizing build times 20 | require('time-grunt')(grunt); 21 | 22 | // Configurable paths for the application 23 | var appConfig = { 24 | app: require('./bower.json').appPath || 'app', 25 | dist: 'client/dist' 26 | }; 27 | 28 | // Define the configuration for all the tasks 29 | grunt.initConfig({ 30 | 31 | // Project settings 32 | yeoman: appConfig, 33 | 34 | // Watches files for changes and runs tasks based on the changed files 35 | watch: { 36 | bower: { 37 | files: ['bower.json'], 38 | tasks: ['wiredep'] 39 | }, 40 | js: { 41 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], 42 | tasks: ['newer:jshint:all'], 43 | options: { 44 | livereload: '<%= connect.options.livereload %>' 45 | } 46 | }, 47 | jsTest: { 48 | files: ['<%= yeoman.app %>/test/spec/{,*/}*.js'], 49 | tasks: ['newer:jshint:test', 'karma'] 50 | }, 51 | styles: { 52 | files: ['<%= yeoman.app %>/styles/{,*/}*.css'], 53 | tasks: ['newer:copy:styles', 'autoprefixer'] 54 | }, 55 | gruntfile: { 56 | files: ['Gruntfile.js'] 57 | }, 58 | livereload: { 59 | options: { 60 | livereload: '<%= connect.options.livereload %>' 61 | }, 62 | files: [ 63 | '<%= yeoman.app %>/{,*/}*.html', 64 | '.tmp/styles/{,*/}*.css', 65 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 66 | ] 67 | }, 68 | lbclient: { 69 | files: [ 70 | 'lbclient/models/*', 71 | 'lbclient/app*', 72 | 'lbclient/datasources*', 73 | 'lbclient/models*', 74 | 'lbclient/build.js' 75 | ], 76 | tasks: ['build-lbclient'], 77 | options: { 78 | livereload: '<%= connect.options.livereload %>' 79 | }, 80 | }, 81 | react: { 82 | files: [ 83 | '<%= yeoman.app %>/**/*.jsx' 84 | ], 85 | tasks: ['react'], 86 | options: { 87 | livereload: '<%= connect.options.livereload %>' 88 | }, 89 | }, 90 | config: { 91 | files: ['<%= yeoman.app %>/config/*.json'], 92 | tasks: ['build-config'], 93 | options: { 94 | livereload: '<%= connect.options.livereload %>' 95 | }, 96 | }, 97 | }, 98 | 99 | // The actual grunt server settings 100 | connect: { 101 | options: { 102 | port: 3000, 103 | // Change this to '0.0.0.0' to access the server from outside. 104 | hostname: 'localhost', 105 | livereload: 35729 106 | }, 107 | test: { 108 | options: { 109 | port: 9001, 110 | middleware: function (connect) { 111 | return [ 112 | connect.static('.tmp'), 113 | connect.static('test'), 114 | connect().use( 115 | '/bower_components', 116 | connect.static('./bower_components') 117 | ), 118 | connect().use( 119 | '/lbclient', 120 | connect.static('./lbclient') 121 | ), 122 | connect.static(appConfig.app) 123 | ]; 124 | } 125 | } 126 | } 127 | }, 128 | 129 | // Make sure code styles are up to par and there are no obvious mistakes 130 | jshint: { 131 | options: { 132 | jshintrc: '.jshintrc', 133 | reporter: require('jshint-stylish') 134 | }, 135 | all: { 136 | src: [ 137 | 'Gruntfile.js', 138 | '<%= yeoman.app %>/scripts/{,*/}*.js' 139 | ] 140 | }, 141 | test: { 142 | options: { 143 | jshintrc: '<%= yeoman.app %>/test/.jshintrc' 144 | }, 145 | src: ['test/spec/{,*/}*.js'] 146 | } 147 | }, 148 | 149 | // Empties folders to start fresh 150 | clean: { 151 | dist: { 152 | files: [{ 153 | dot: true, 154 | src: [ 155 | '.tmp', 156 | '<%= yeoman.dist %>/{,*/}*', 157 | '!<%= yeoman.dist %>/.git*' 158 | ] 159 | }] 160 | }, 161 | server: '.tmp', 162 | lbclient: 'lbclient/browser.bundle.js', 163 | config: '<%= yeoman.app %>/config/bundle.js' 164 | }, 165 | 166 | // Add vendor prefixed styles 167 | autoprefixer: { 168 | options: { 169 | browsers: ['last 1 version'] 170 | }, 171 | dist: { 172 | files: [{ 173 | expand: true, 174 | cwd: '.tmp/styles/', 175 | src: '{,*/}*.css', 176 | dest: '.tmp/styles/' 177 | }] 178 | } 179 | }, 180 | 181 | // Automatically inject Bower components into the app 182 | wiredep: { 183 | options: { 184 | cwd: '<%= yeoman.app %>', 185 | bowerJson: require('./bower.json'), 186 | directory: './bower_components', //require('./.bowerrc').directory 187 | overrides: { 188 | react: { 189 | main: "react-with-addons.js" 190 | } 191 | } 192 | }, 193 | app: { 194 | src: ['<%= yeoman.app %>/index.html'], 195 | ignorePath: /..\// 196 | } 197 | }, 198 | 199 | // Renames files for browser caching purposes 200 | filerev: { 201 | dist: { 202 | src: [ 203 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 204 | '<%= yeoman.dist %>/styles/{,*/}*.css', 205 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 206 | '<%= yeoman.dist %>/styles/fonts/*' 207 | ] 208 | } 209 | }, 210 | 211 | // Reads HTML for usemin blocks to enable smart builds that automatically 212 | // concat, minify and revision files. Creates configurations in memory so 213 | // additional tasks can operate on them 214 | useminPrepare: { 215 | html: '<%= yeoman.app %>/index.html', 216 | options: { 217 | dest: '<%= yeoman.dist %>', 218 | flow: { 219 | html: { 220 | steps: { 221 | js: ['concat', 'uglifyjs'], 222 | css: ['cssmin'] 223 | }, 224 | post: {} 225 | } 226 | } 227 | } 228 | }, 229 | 230 | // Performs rewrites based on filerev and the useminPrepare configuration 231 | usemin: { 232 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 233 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 234 | options: { 235 | assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images'] 236 | } 237 | }, 238 | 239 | // The following *-min tasks will produce minified files in the dist folder 240 | // By default, your `index.html`'s will take care of 241 | // minification. These next options are pre-configured if you do not wish 242 | // to use the Usemin blocks. 243 | // cssmin: { 244 | // dist: { 245 | // files: { 246 | // '<%= yeoman.dist %>/styles/main.css': [ 247 | // '.tmp/styles/{,*/}*.css' 248 | // ] 249 | // } 250 | // } 251 | // }, 252 | // uglify: { 253 | // dist: { 254 | // files: { 255 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 256 | // '<%= yeoman.dist %>/scripts/scripts.js' 257 | // ] 258 | // } 259 | // } 260 | // }, 261 | // concat: { 262 | // dist: {} 263 | // }, 264 | 265 | imagemin: { 266 | dist: { 267 | files: [{ 268 | expand: true, 269 | cwd: '<%= yeoman.app %>/images', 270 | src: '{,*/}*.{png,jpg,jpeg,gif}', 271 | dest: '<%= yeoman.dist %>/images' 272 | }] 273 | } 274 | }, 275 | 276 | svgmin: { 277 | dist: { 278 | files: [{ 279 | expand: true, 280 | cwd: '<%= yeoman.app %>/images', 281 | src: '{,*/}*.svg', 282 | dest: '<%= yeoman.dist %>/images' 283 | }] 284 | } 285 | }, 286 | 287 | htmlmin: { 288 | dist: { 289 | options: { 290 | collapseWhitespace: true, 291 | conservativeCollapse: true, 292 | collapseBooleanAttributes: true, 293 | removeCommentsFromCDATA: true, 294 | removeOptionalTags: true 295 | }, 296 | files: [{ 297 | expand: true, 298 | cwd: '<%= yeoman.dist %>', 299 | src: ['*.html', 'views/{,*/}*.html'], 300 | dest: '<%= yeoman.dist %>' 301 | }] 302 | } 303 | }, 304 | 305 | // ngAnnotate tries to make the code safe for minification automatically by 306 | // using the Angular long form for dependency injection. It doesn't work on 307 | // things like resolve or inject so those have to be done manually. 308 | ngAnnotate: { 309 | dist: { 310 | files: [{ 311 | expand: true, 312 | cwd: '.tmp/concat/scripts', 313 | src: '*.js', 314 | dest: '.tmp/concat/scripts' 315 | }] 316 | } 317 | }, 318 | 319 | // Compile React's JSX files 320 | react: { 321 | dynamic_mappings: { 322 | files: [ 323 | { 324 | expand: true, 325 | cwd: 'client/reapp', 326 | src: ['**/*.jsx'], 327 | dest: '.tmp', 328 | ext: '.js' 329 | } 330 | ] 331 | } 332 | }, 333 | 334 | // Replace Google CDN references 335 | cdnify: { 336 | dist: { 337 | html: ['<%= yeoman.dist %>/*.html'] 338 | } 339 | }, 340 | 341 | // Copies remaining files to places other tasks can use 342 | copy: { 343 | dist: { 344 | files: [{ 345 | expand: true, 346 | dot: true, 347 | cwd: '<%= yeoman.app %>', 348 | dest: '<%= yeoman.dist %>', 349 | src: [ 350 | '*.{ico,png,txt}', 351 | '.htaccess', 352 | '*.html', 353 | 'views/{,*/}*.html', 354 | 'images/{,*/}*.{webp}', 355 | 'fonts/*' 356 | ] 357 | }, { 358 | expand: true, 359 | cwd: '.tmp/images', 360 | dest: '<%= yeoman.dist %>/images', 361 | src: ['generated/*'] 362 | }] 363 | }, 364 | styles: { 365 | expand: true, 366 | cwd: '<%= yeoman.app %>/styles', 367 | dest: '.tmp/styles/', 368 | src: '{,*/}*.css' 369 | } 370 | }, 371 | 372 | // Run some tasks in parallel to speed up the build process 373 | concurrent: { 374 | server: [ 375 | 'copy:styles' 376 | ], 377 | test: [ 378 | 'copy:styles' 379 | ], 380 | dist: [ 381 | 'copy:styles', 382 | 'imagemin', 383 | 'svgmin' 384 | ] 385 | }, 386 | 387 | // Test settings 388 | karma: { 389 | unit: { 390 | configFile: '<%= yeoman.app %>/test/karma.conf.js', 391 | browsers: [ 'PhantomJS' ], 392 | singleRun: true 393 | } 394 | } 395 | }); 396 | 397 | grunt.registerTask('build-lbclient', 'Build lbclient browser bundle', function() { 398 | var done = this.async(); 399 | buildClientBundle(process.env.NODE_ENV || 'development', done); 400 | }); 401 | 402 | grunt.registerTask('build-config', 'Build confg.js from JSON files', function() { 403 | var ngapp = path.resolve(__dirname, appConfig.app); 404 | var configDir = path.join(ngapp, 'config'); 405 | var config = {}; 406 | 407 | fs.readdirSync(configDir) 408 | .forEach(function(f) { 409 | if (f === 'bundle.js') return; 410 | 411 | var extname = path.extname(f); 412 | if (extname !== '.json') { 413 | grunt.warn('Ignoring ' + f + ' (' + extname + ')'); 414 | return; 415 | } 416 | 417 | var fullPath = path.resolve(configDir, f); 418 | var key = path.basename(f, extname); 419 | 420 | config[key] = JSON.parse(fs.readFileSync(fullPath), 'utf-8'); 421 | }); 422 | 423 | var outputPath = path.resolve(ngapp, 'config', 'bundle.js'); 424 | var content = 'window.CONFIG = ' + 425 | JSON.stringify(config, null, 2) + 426 | ';\n'; 427 | fs.writeFileSync(outputPath, content, 'utf-8'); 428 | }); 429 | 430 | grunt.registerTask('run', 'Start the app server', function() { 431 | var done = this.async(); 432 | 433 | var connectConfig = grunt.config.get().connect.options; 434 | process.env.LIVE_RELOAD = connectConfig.livereload; 435 | process.env.NODE_ENV = this.args[0]; 436 | 437 | var keepAlive = this.flags.keepalive || connectConfig.keepalive; 438 | 439 | var server = require('./server'); 440 | server.set('port', connectConfig.port); 441 | server.set('host', connectConfig.hostname); 442 | server.start() 443 | .on('listening', function() { 444 | if (!keepAlive) done(); 445 | }) 446 | .on('error', function(err) { 447 | if (err.code === 'EADDRINUSE') { 448 | grunt.fatal('Port ' + connectConfig.port + 449 | ' is already in use by another process.'); 450 | } else { 451 | grunt.fatal(err); 452 | } 453 | }); 454 | }); 455 | 456 | grunt.registerTask('serve', 'Compile then start the app server', function (target) { 457 | if (target === 'dist') { 458 | return grunt.task.run(['build', 'run:dist:keepalive']); 459 | } 460 | 461 | grunt.task.run([ 462 | 'clean:server', 463 | 'build-lbclient', 464 | 'build-config', 465 | 'react', 466 | 'wiredep', 467 | 'concurrent:server', 468 | 'autoprefixer', 469 | 'run:development', 470 | 'watch' 471 | ]); 472 | }); 473 | 474 | grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) { 475 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 476 | grunt.task.run(['serve:' + target]); 477 | }); 478 | 479 | grunt.registerTask('test', [ 480 | 'clean:server', 481 | 'build-lbclient', 482 | 'build-config', 483 | 'concurrent:test', 484 | 'autoprefixer', 485 | 'connect:test', 486 | 'karma' 487 | ]); 488 | 489 | grunt.registerTask('build', [ 490 | 'clean:dist', 491 | 'build-lbclient', 492 | 'build-config', 493 | 'react', 494 | 'wiredep', 495 | 'useminPrepare', 496 | 'concurrent:dist', 497 | 'autoprefixer', 498 | 'concat', 499 | 'ngAnnotate', 500 | 'copy:dist', 501 | 'cdnify', 502 | 'cssmin', 503 | 'uglify', 504 | 'filerev', 505 | 'usemin', 506 | 'htmlmin' 507 | ]); 508 | 509 | grunt.registerTask('default', [ 510 | 'newer:jshint', 511 | 'test', 512 | 'build' 513 | ]); 514 | }; 515 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loopback Examples: Full Stack 2 | 3 | > This repo reimplements [Loopback Full Stack Example](https://github.com/strongloop/loopback-example-full-stack/) using React instead of Angular. The React app is located in `client/reapp`. It uses [react-router](https://github.com/rackt/react-router) for routing and [reflux](https://github.com/spoike/refluxjs) implementation of Flux unidirectional data flow. 4 | 5 | --- 6 | 7 | **Note: this example uses `loopback@2.0.0` and `loopback-boot@2.0.0`!** 8 | 9 | An example running LoopBack in the browser and server, demonstrating the 10 | following features: 11 | 12 | - offline data access and synchronization 13 | - routes shared between the AngularJS app and the HTTP server 14 | 15 | ## Install and Run 16 | 17 | 0. You must have `node` and `git` installed. It's recommended to have `mongod` 18 | installed too, so that the data is preserved across app restarts. 19 | 20 | 1. Clone the repo. 21 | 22 | 2. `cd loopback-example-full-stack` 23 | 24 | 3. `npm install` - install the root package dependencies 25 | 26 | 4. `npm install grunt-cli -g` - skip if you have grunt-cli already installed 27 | 28 | 5. `npm install bower -g` - skip if you have bower already installed 29 | 30 | 6. `bower install` - install front-end scripts 31 | 32 | 7. `mongod` - make sure mongodb is running if you want to run with 33 | `NODE_ENV=production` 34 | 35 | 8. `grunt serve` - build and run the entire project in development mode 36 | 37 | 9. open `http://localhost:3000` - point a browser at the running app 38 | 39 | ## Project layout 40 | 41 | The project is composed from multiple components. 42 | 43 | - `models/` contains definition of models that are shared by both the server 44 | and the client. 45 | 46 | - `rest/` contains the REST API server, it exposes the shared models via 47 | REST API. 48 | 49 | - `lbclient/` provides an isomorphic loopback client with offline sync. 50 | The client needs some client-only models for data synchronization, these 51 | models are defined in `lbclient/models/`. 52 | 53 | - `ngapp/` is a single-page AngularJS application scaffolded using `yo 54 | angular`, with few modification to make it work better in the full-stack 55 | project. 56 | 57 | - `server/` is the main HTTP server that brings together all other components. 58 | 59 | ## Build 60 | 61 | This project uses [Grunt](http://gruntjs.com) for the build, since that's what 62 | `yo angular` creates. 63 | 64 | There are three major changes from the vanilla Gruntfile required for this 65 | full-stack example: 66 | 67 | - `grunt serve` uses the `server/` component instead of `grunt connect`. 68 | 69 | - `lbclient` component provides a custom build script (`lbclient/build.js`) 70 | which runs `browserify` to produce a single js file to be used in the 71 | browser. The Gruntfile contains a custom task to run this build. 72 | 73 | - The definition of Angular routes is kept in a standalone JSON file 74 | that is used by the `server/` component too. To make this JSON file 75 | available in the browser, there is a custom task that builds 76 | `ngapp/config/bundle.js`. 77 | 78 | ### Targets 79 | 80 | - `grunt serve` starts the app in development mode, watching for file changes 81 | and automatically reloading the app. 82 | - `grunt test` runs automated tests (only the front-end has tests at the 83 | moment). 84 | - `grunt build` creates the bundle for deploying to production. 85 | - `grunt serve:dist` starts the app serving the production bundle of the 86 | front-end SPA. 87 | - `grunt jshint` checks consistency of the coding style. 88 | 89 | ## Adding more features 90 | 91 | #### Define a new shared model 92 | 93 | The instructions assume the name of the new model is 'MyModel'. 94 | 95 | 1. Create a file `models/my-model.json`, put the model definition there. 96 | Use `models/todo.json` as an example, see 97 | [loopback-boot docs](http://apidocs.strongloop.com/loopback-boot) for 98 | more details about the file format. 99 | 100 | 2. (Optional) Add `models/my-model.js` and implement your custom model 101 | methods. See `models/todo.js` for an example. 102 | 103 | 3. Add an entry to `rest/models.json` to configure the new model in the REST 104 | server: 105 | 106 | ```json 107 | { 108 | "MyModel": { 109 | "dataSource": "db" 110 | } 111 | } 112 | ``` 113 | 114 | 4. Define a client-only model to represent the remote server model in the 115 | client - create `lbclient/models/my-model.json` with the following content: 116 | 117 | ```json 118 | { 119 | "name": "RemoteMyModel", 120 | "base": "MyModel" 121 | } 122 | ``` 123 | 124 | 5. Add two entries to `lbclient/models.json` to configure the new models 125 | for the client: 126 | 127 | ```json 128 | { 129 | "MyModel": { 130 | "dataSource": "local" 131 | }, 132 | "RemoteMyModel": { 133 | "dataSource": "remote" 134 | } 135 | } 136 | ``` 137 | 138 | 6. Register the local model with Angular's injector in 139 | `ngapp/scripts/services/lbclient.js`: 140 | 141 | ```js 142 | .value('MyModel', app.models.LocalMyModel) 143 | ``` 144 | 145 | ### Create a new Angular route 146 | 147 | Since the full-stack example project shares the routes between the client and 148 | the server, the new route cannot be added using the yeoman generator. 149 | 150 | Instructions: 151 | 152 | 1. (Optional) Create a new angular controller using yeoman, e.g. 153 | 154 | ```sh 155 | $ yo angular:controller MyModel 156 | ``` 157 | 158 | 2. (Optional) Create a new angular view using yeoman, e.g. 159 | 160 | ```sh 161 | $ yo angular:view models 162 | ``` 163 | 164 | 3. Add a route entry to `ngapp/config/routes.json`, e.g.: 165 | 166 | ```json 167 | { 168 | "/models": { 169 | "controller": "MymodelCtrl", 170 | "templateUrl": "/views/models.html" 171 | } 172 | } 173 | ``` 174 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-example-full-stack", 3 | "version": "0.0.0", 4 | "homepage": "https://github.com/strongloop/loopback-example-full-stack", 5 | "authors": [ 6 | "Sam Roberts " 7 | ], 8 | "license": "MIT", 9 | "private": true, 10 | "appPath": "client/reapp", 11 | "testPath": "client/reapp/test/spec", 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "**/bower_components", 16 | "test", 17 | "tests" 18 | ], 19 | "dependencies": { 20 | "json3": "~3.3.1", 21 | "es5-shim": "~3.1.0", 22 | "react": "~0.12.2", 23 | "react-router": "~0.11.6", 24 | "reflux": "~0.2.3" 25 | }, 26 | "devDependencies": { 27 | "angular-mocks": "~1.2.16", 28 | "angular-scenario": "~1.2.16" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/lbclient/.gitignore: -------------------------------------------------------------------------------- 1 | browser.bundle.js 2 | -------------------------------------------------------------------------------- /client/lbclient/boot/replication.js: -------------------------------------------------------------------------------- 1 | // TODO(bajtos) Move the bi-di replication to loopback core, 2 | // add model settings to enable the replication. 3 | // Example: 4 | // LocalTodo: { options: { 5 | // base: 'Todo', 6 | // replicate: { 7 | // target: 'Todo', 8 | // mode: 'push' | 'pull' | 'bidi' 9 | // }}} 10 | module.exports = function(client) { 11 | var LocalTodo = client.models.LocalTodo; 12 | var RemoteTodo = client.models.Todo; 13 | 14 | client.network = { 15 | _isConnected: true, 16 | get isConnected() { 17 | console.log('isConnected?', this._isConnected); 18 | return this._isConnected; 19 | }, 20 | set isConnected(value) { 21 | this._isConnected = value; 22 | } 23 | }; 24 | 25 | // setup model replication 26 | function sync(cb) { 27 | if (client.network.isConnected) { 28 | RemoteTodo.replicate(LocalTodo, function() { 29 | LocalTodo.replicate(RemoteTodo, cb); 30 | }); 31 | } 32 | } 33 | 34 | // sync local changes if connected 35 | LocalTodo.on('changed', sync); 36 | LocalTodo.on('deleted', sync); 37 | 38 | client.sync = sync; 39 | }; 40 | -------------------------------------------------------------------------------- /client/lbclient/build.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var pkg = require('./package.json'); 3 | var fs = require('fs'); 4 | var browserify = require('browserify'); 5 | var boot = require('loopback-boot'); 6 | 7 | module.exports = function buildBrowserBundle(env, callback) { 8 | var b = browserify({ basedir: __dirname }); 9 | b.require('./' + pkg.main, { expose: 'lbclient' }); 10 | 11 | try { 12 | boot.compileToBrowserify({ 13 | appRootDir: __dirname, 14 | env: env 15 | }, b); 16 | } catch(err) { 17 | return callback(err); 18 | } 19 | 20 | var bundlePath = path.resolve(__dirname, 'browser.bundle.js'); 21 | var out = fs.createWriteStream(bundlePath); 22 | var isDevEnv = ~['debug', 'development', 'test'].indexOf(env); 23 | 24 | b.bundle({ 25 | // TODO(bajtos) debug should be always true, the sourcemaps should be 26 | // saved to a standalone file when !isDev(env) 27 | debug: isDevEnv 28 | }) 29 | .on('error', callback) 30 | .pipe(out); 31 | 32 | out.on('error', callback); 33 | out.on('close', callback); 34 | }; 35 | -------------------------------------------------------------------------------- /client/lbclient/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "remote": { 3 | "connector": "remote" 4 | }, 5 | "local": { 6 | "connector": "memory", 7 | "localStorage": "todo-db" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/lbclient/datasources.local.js: -------------------------------------------------------------------------------- 1 | var GLOBAL_CONFIG = require('../../global-config'); 2 | 3 | module.exports = { 4 | remote: { 5 | url: GLOBAL_CONFIG.restApiUrl 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /client/lbclient/lbclient.js: -------------------------------------------------------------------------------- 1 | var loopback = require('loopback'); 2 | var boot = require('loopback-boot'); 3 | 4 | var client = module.exports = loopback(); 5 | boot(client); 6 | -------------------------------------------------------------------------------- /client/lbclient/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": ["../../common/models", "./models"] 4 | }, 5 | "Todo": { 6 | "dataSource": "remote" 7 | }, 8 | "LocalTodo": { 9 | "dataSource": "local" 10 | }, 11 | "user": { 12 | "dataSource": "remote" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/lbclient/models/local-todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LocalTodo", 3 | "base": "Todo" 4 | } 5 | -------------------------------------------------------------------------------- /client/lbclient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "main": "lbclient.js" 4 | } 5 | -------------------------------------------------------------------------------- /client/ngapp/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /client/ngapp/config/bundle.js: -------------------------------------------------------------------------------- 1 | window.CONFIG = { 2 | "routes": { 3 | "/": { 4 | "controller": "HomeCtrl", 5 | "templateUrl": "/views/welcome.html" 6 | }, 7 | "/me": { 8 | "controller": "UserCtrl", 9 | "templateUrl": "/views/user.html" 10 | }, 11 | "/my/todos/:status": { 12 | "controller": "TodoCtrl", 13 | "templateUrl": "/views/todos.html" 14 | }, 15 | "/my/todos": { 16 | "controller": "TodoCtrl", 17 | "templateUrl": "/views/todos.html" 18 | }, 19 | "/login": { 20 | "controller": "LoginCtrl", 21 | "templateUrl": "/views/login.html" 22 | }, 23 | "/register": { 24 | "controller": "RegisterCtrl", 25 | "templateUrl": "/views/register.html" 26 | }, 27 | "/debug": { 28 | "controller": "ChangeCtrl", 29 | "templateUrl": "/views/changes.html" 30 | } 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /client/ngapp/config/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": { 3 | "controller": "HomeCtrl", 4 | "templateUrl": "/views/welcome.html" 5 | }, 6 | "/me": { 7 | "controller": "UserCtrl", 8 | "templateUrl": "/views/user.html" 9 | }, 10 | "/my/todos/:status": { 11 | "controller": "TodoCtrl", 12 | "templateUrl": "/views/todos.html" 13 | }, 14 | "/my/todos": { 15 | "controller": "TodoCtrl", 16 | "templateUrl": "/views/todos.html" 17 | }, 18 | "/login": { 19 | "controller": "LoginCtrl", 20 | "templateUrl": "/views/login.html" 21 | }, 22 | "/register": { 23 | "controller": "RegisterCtrl", 24 | "templateUrl": "/views/register.html" 25 | }, 26 | "/debug": { 27 | "controller": "ChangeCtrl", 28 | "templateUrl": "/views/changes.html" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/ngapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dundalek/loopback-example-full-stack-react/f5d63172ee1e3e19553dc1a879b9a541c6f17730/client/ngapp/favicon.ico -------------------------------------------------------------------------------- /client/ngapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo App 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 |

Welcome to the Todo App

32 | 33 |
34 | 39 |
40 | 41 |
42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /client/ngapp/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /client/ngapp/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc overview 5 | * @name loopbackExampleFullStackApp 6 | * @description 7 | * # loopbackExampleFullStackApp 8 | * 9 | * Main module of the application. 10 | */ 11 | angular 12 | .module('loopbackExampleFullStackApp', [ 13 | 'ngRoute' 14 | ]) 15 | .config(function ($routeProvider, $locationProvider) { 16 | Object.keys(window.CONFIG.routes) 17 | .forEach(function(route) { 18 | var routeDef = window.CONFIG.routes[route]; 19 | $routeProvider.when(route, routeDef); 20 | }); 21 | 22 | $routeProvider 23 | .otherwise({ 24 | redirectTo: '/' 25 | }); 26 | 27 | $locationProvider.html5Mode(true); 28 | }); 29 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/change.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name loopbackExampleFullStackApp.controller:ChangeCtrl 6 | * @description 7 | * # ChangeCtrl 8 | * Controller of the loopbackExampleFullStackApp 9 | */ 10 | angular.module('loopbackExampleFullStackApp') 11 | .controller('ChangeCtrl', function ChangeCtrl($scope, $routeParams, $filter, 12 | Todo, RemoteTodo) { 13 | 14 | Todo.getChangeModel().find(function(err, changes) { 15 | $scope.changes = changes; 16 | $scope.$apply(); 17 | 18 | RemoteTodo.diff(0, changes, function(err, diff) { 19 | $scope.diff = diff; 20 | $scope.$apply(); 21 | }); 22 | }); 23 | 24 | $scope.clearLocalStorage = function() { 25 | localStorage.removeItem('todo-db'); 26 | }; 27 | 28 | Todo.find(function(err, todos) { 29 | $scope.todos = todos; 30 | $scope.$apply(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name loopbackExampleFullStackApp.controller:HomeCtrl 6 | * @description 7 | * # HomeCtrl 8 | * Controller of the loopbackExampleFullStackApp 9 | */ 10 | angular.module('loopbackExampleFullStackApp') 11 | .controller('HomeCtrl', function ($scope) { 12 | $scope.foo = Math.random(); 13 | }); 14 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name loopbackExampleFullStackApp.controller:LoginCtrl 6 | * @description 7 | * # LoginCtrl 8 | * Controller of the loopbackExampleFullStackApp 9 | */ 10 | angular.module('loopbackExampleFullStackApp') 11 | .controller('LoginCtrl', function ($scope) { 12 | $scope.foo = Math.random(); 13 | }); 14 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name loopbackExampleFullStackApp.controller:RegisterCtrl 6 | * @description 7 | * # RegisterCtrl 8 | * Controller of the loopbackExampleFullStackApp 9 | */ 10 | angular.module('loopbackExampleFullStackApp') 11 | .controller('RegisterCtrl', function ($scope) { 12 | $scope.foo = Math.random(); 13 | }); 14 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/todo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name loopbackExampleFullStackApp.controller:TodoCtrl 6 | * @description 7 | * # TodoCtrl 8 | * Controller of the loopbackExampleFullStackApp 9 | */ 10 | angular.module('loopbackExampleFullStackApp') 11 | .controller('TodoCtrl', function TodoCtrl($scope, $routeParams, $filter, Todo, 12 | $location, sync, network) { 13 | $scope.todos = []; 14 | 15 | $scope.newTodo = ''; 16 | $scope.editedTodo = null; 17 | 18 | // sync the initial data 19 | sync(onChange); 20 | 21 | // the location service 22 | $scope.loc = $location; 23 | 24 | function onChange() { 25 | Todo.stats(function(err, stats) { 26 | if(err) return error(err); 27 | $scope.stats = stats; 28 | }); 29 | Todo.find({ 30 | where: $scope.statusFilter, 31 | sort: 'order DESC' 32 | }, function(err, todos) { 33 | $scope.todos = todos; 34 | $scope.$apply(); 35 | }); 36 | } 37 | 38 | function error(err) { 39 | //TODO error handling 40 | throw err; 41 | } 42 | 43 | function errorCallback(err) { 44 | if(err) error(err); 45 | } 46 | 47 | Todo.on('changed', onChange); 48 | Todo.on('deleted', onChange); 49 | 50 | // Monitor the current route for changes and adjust the filter accordingly. 51 | $scope.$on('$routeChangeSuccess', function () { 52 | var status = $scope.status = $routeParams.status || ''; 53 | $scope.statusFilter = (status === 'active') ? 54 | { completed: false } : (status === 'completed') ? 55 | { completed: true } : {}; 56 | }); 57 | 58 | $scope.addTodo = function () { 59 | var todo = new Todo({title: $scope.newTodo}); 60 | todo.save(); 61 | $scope.newTodo = ''; 62 | }; 63 | 64 | $scope.editTodo = function (todo) { 65 | $scope.editedTodo = todo; 66 | }; 67 | 68 | $scope.todoCompleted = function(todo) { 69 | todo.completed = true; 70 | todo.save(); 71 | }; 72 | 73 | $scope.doneEditing = function (todo) { 74 | $scope.editedTodo = null; 75 | todo.title = todo.title.trim(); 76 | 77 | if (!todo.title) { 78 | $scope.removeTodo(todo); 79 | } else { 80 | todo.save(); 81 | } 82 | }; 83 | 84 | $scope.removeTodo = function (todo) { 85 | todo.remove(errorCallback); 86 | }; 87 | 88 | $scope.clearCompletedTodos = function () { 89 | Todo.destroyAll({completed: true}, onChange); 90 | }; 91 | 92 | $scope.markAll = function (completed) { 93 | Todo.find(function(err, todos) { 94 | if(err) return errorCallback(err); 95 | todos.forEach(function(todo) { 96 | todo.completed = completed; 97 | todo.save(errorCallback); 98 | }); 99 | }); 100 | }; 101 | 102 | $scope.sync = function() { 103 | sync(); 104 | }; 105 | 106 | $scope.connected = function() { 107 | return network.isConnected; 108 | }; 109 | 110 | $scope.connect = function() { 111 | network.isConnected = true; 112 | sync(); 113 | }; 114 | 115 | $scope.disconnect = function() { 116 | network.isConnected = false; 117 | }; 118 | 119 | Todo.on('conflicts', function(conflicts) { 120 | $scope.localConflicts = conflicts; 121 | conflicts.forEach(function(conflict) { 122 | conflict.type(function(err, type) { 123 | conflict.type = type; 124 | conflict.models(function(err, source, target) { 125 | conflict.source = source; 126 | conflict.target = target; 127 | conflict.manual = new conflict.SourceModel(source || target); 128 | $scope.$apply(); 129 | }); 130 | conflict.changes(function(err, source, target) { 131 | conflict.sourceChange = source; 132 | conflict.targetChange = target; 133 | $scope.$apply(); 134 | }); 135 | }); 136 | }); 137 | }); 138 | 139 | $scope.resolveUsingSource = function(conflict) { 140 | conflict.resolve(refreshConflicts); 141 | }; 142 | 143 | $scope.resolveUsingTarget = function(conflict) { 144 | if(conflict.targetChange.type() === 'delete') { 145 | conflict.SourceModel.deleteById(conflict.modelId, refreshConflicts); 146 | } else { 147 | var m = new conflict.SourceModel(conflict.target); 148 | m.save(refreshConflicts); 149 | } 150 | }; 151 | 152 | $scope.resolveManually = function(conflict) { 153 | conflict.manual.save(function(err) { 154 | if(err) return errorCallback(err); 155 | conflict.resolve(refreshConflicts); 156 | }); 157 | }; 158 | 159 | function refreshConflicts() { 160 | $scope.localConflicts = []; 161 | $scope.$apply(); 162 | sync(); 163 | } 164 | }); 165 | -------------------------------------------------------------------------------- /client/ngapp/scripts/controllers/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name loopbackExampleFullStackApp.controller:UserCtrl 6 | * @description 7 | * # UserCtrl 8 | * Controller of the loopbackExampleFullStackApp 9 | */ 10 | angular.module('loopbackExampleFullStackApp') 11 | .controller('UserCtrl', function ($scope) { 12 | $scope.foo = Math.random(); 13 | }); 14 | -------------------------------------------------------------------------------- /client/ngapp/scripts/services/lbclient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // load lbclient via browserify's require 4 | var client = (function() { 5 | /*global require:true*/ 6 | return require('lbclient'); 7 | })(); 8 | 9 | /** 10 | * @ngdoc service 11 | * @name loopbackExampleFullStackApp.lbclient 12 | * @description 13 | * # lbclient 14 | * Value in the loopbackExampleFullStackApp. 15 | */ 16 | angular.module('loopbackExampleFullStackApp') 17 | .value('Todo', client.models.LocalTodo) 18 | .value('RemoteTodo', client.models.Todo) 19 | .value('sync', client.sync) 20 | .value('network', client.network); 21 | -------------------------------------------------------------------------------- /client/ngapp/styles/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | color: inherit; 16 | -webkit-appearance: none; 17 | -ms-appearance: none; 18 | -o-appearance: none; 19 | appearance: none; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #eaeaea; 26 | color: #4d4d4d; 27 | width: 550px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -ms-font-smoothing: antialiased; 32 | -o-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | } 35 | 36 | .header { 37 | border-bottom: 1px solid #e5e5e5; 38 | } 39 | 40 | .header .navigation { 41 | padding-left: 0; 42 | list-style-type: none; 43 | } 44 | 45 | .header .navigation li { 46 | display: inline; 47 | } 48 | 49 | button, 50 | input[type="checkbox"] { 51 | outline: none; 52 | } 53 | 54 | #todoapp { 55 | background: #fff; 56 | background: rgba(255, 255, 255, 0.9); 57 | margin: 130px 0 40px 0; 58 | border: 1px solid #ccc; 59 | position: relative; 60 | border-top-left-radius: 2px; 61 | border-top-right-radius: 2px; 62 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 63 | 0 25px 50px 0 rgba(0, 0, 0, 0.15); 64 | } 65 | 66 | #todoapp:before { 67 | content: ''; 68 | border-left: 1px solid #f5d6d6; 69 | border-right: 1px solid #f5d6d6; 70 | width: 2px; 71 | position: absolute; 72 | top: 0; 73 | left: 40px; 74 | height: 100%; 75 | } 76 | 77 | #todoapp input::-webkit-input-placeholder { 78 | font-style: italic; 79 | } 80 | 81 | #todoapp input::-moz-placeholder { 82 | font-style: italic; 83 | color: #a9a9a9; 84 | } 85 | 86 | #todoapp h1 { 87 | position: absolute; 88 | top: -120px; 89 | width: 100%; 90 | font-size: 70px; 91 | font-weight: bold; 92 | text-align: center; 93 | color: #b3b3b3; 94 | color: rgba(255, 255, 255, 0.3); 95 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 96 | -webkit-text-rendering: optimizeLegibility; 97 | -moz-text-rendering: optimizeLegibility; 98 | -ms-text-rendering: optimizeLegibility; 99 | -o-text-rendering: optimizeLegibility; 100 | text-rendering: optimizeLegibility; 101 | } 102 | 103 | #header { 104 | padding-top: 15px; 105 | border-radius: inherit; 106 | } 107 | 108 | #header:before { 109 | content: ''; 110 | position: absolute; 111 | top: 0; 112 | right: 0; 113 | left: 0; 114 | height: 15px; 115 | z-index: 2; 116 | border-bottom: 1px solid #6c615c; 117 | background: #8d7d77; 118 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 119 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 120 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 121 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 122 | border-top-left-radius: 1px; 123 | border-top-right-radius: 1px; 124 | } 125 | 126 | #new-todo, 127 | .edit { 128 | position: relative; 129 | margin: 0; 130 | width: 100%; 131 | font-size: 24px; 132 | font-family: inherit; 133 | line-height: 1.4em; 134 | border: 0; 135 | outline: none; 136 | color: inherit; 137 | padding: 6px; 138 | border: 1px solid #999; 139 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 140 | -moz-box-sizing: border-box; 141 | -ms-box-sizing: border-box; 142 | -o-box-sizing: border-box; 143 | box-sizing: border-box; 144 | -webkit-font-smoothing: antialiased; 145 | -moz-font-smoothing: antialiased; 146 | -ms-font-smoothing: antialiased; 147 | -o-font-smoothing: antialiased; 148 | font-smoothing: antialiased; 149 | } 150 | 151 | #new-todo { 152 | padding: 16px 16px 16px 60px; 153 | border: none; 154 | background: rgba(0, 0, 0, 0.02); 155 | z-index: 2; 156 | box-shadow: none; 157 | } 158 | 159 | #main { 160 | position: relative; 161 | z-index: 2; 162 | border-top: 1px dotted #adadad; 163 | } 164 | 165 | label[for='toggle-all'] { 166 | display: none; 167 | } 168 | 169 | #toggle-all { 170 | position: absolute; 171 | top: -42px; 172 | left: -4px; 173 | width: 40px; 174 | text-align: center; 175 | /* Mobile Safari */ 176 | border: none; 177 | } 178 | 179 | #toggle-all:before { 180 | content: '»'; 181 | font-size: 28px; 182 | color: #d9d9d9; 183 | padding: 0 25px 7px; 184 | } 185 | 186 | #toggle-all:checked:before { 187 | color: #737373; 188 | } 189 | 190 | #todo-list { 191 | margin: 0; 192 | padding: 0; 193 | list-style: none; 194 | } 195 | 196 | #todo-list li { 197 | position: relative; 198 | font-size: 24px; 199 | border-bottom: 1px dotted #ccc; 200 | } 201 | 202 | #todo-list li:last-child { 203 | border-bottom: none; 204 | } 205 | 206 | #todo-list li.editing { 207 | border-bottom: none; 208 | padding: 0; 209 | } 210 | 211 | #todo-list li.editing .edit { 212 | display: block; 213 | width: 506px; 214 | padding: 13px 17px 12px 17px; 215 | margin: 0 0 0 43px; 216 | } 217 | 218 | #todo-list li.editing .view { 219 | display: none; 220 | } 221 | 222 | #todo-list li .toggle { 223 | text-align: center; 224 | width: 40px; 225 | /* auto, since non-WebKit browsers doesn't support input styling */ 226 | height: auto; 227 | position: absolute; 228 | top: 0; 229 | bottom: 0; 230 | margin: auto 0; 231 | /* Mobile Safari */ 232 | border: none; 233 | -webkit-appearance: none; 234 | -ms-appearance: none; 235 | -o-appearance: none; 236 | appearance: none; 237 | } 238 | 239 | #todo-list li .toggle:after { 240 | content: '✔'; 241 | /* 40 + a couple of pixels visual adjustment */ 242 | line-height: 43px; 243 | font-size: 20px; 244 | color: #d9d9d9; 245 | text-shadow: 0 -1px 0 #bfbfbf; 246 | } 247 | 248 | #todo-list li .toggle:checked:after { 249 | color: #85ada7; 250 | text-shadow: 0 1px 0 #669991; 251 | bottom: 1px; 252 | position: relative; 253 | } 254 | 255 | #todo-list li label { 256 | white-space: pre; 257 | word-break: break-word; 258 | padding: 15px 60px 15px 15px; 259 | margin-left: 45px; 260 | display: block; 261 | line-height: 1.2; 262 | -webkit-transition: color 0.4s; 263 | transition: color 0.4s; 264 | } 265 | 266 | #todo-list li.completed label { 267 | color: #a9a9a9; 268 | text-decoration: line-through; 269 | } 270 | 271 | #todo-list li .destroy { 272 | display: none; 273 | position: absolute; 274 | top: 0; 275 | right: 10px; 276 | bottom: 0; 277 | width: 40px; 278 | height: 40px; 279 | margin: auto 0; 280 | font-size: 22px; 281 | color: #a88a8a; 282 | -webkit-transition: all 0.2s; 283 | transition: all 0.2s; 284 | } 285 | 286 | #todo-list li .destroy:hover { 287 | text-shadow: 0 0 1px #000, 288 | 0 0 10px rgba(199, 107, 107, 0.8); 289 | -webkit-transform: scale(1.3); 290 | -ms-transform: scale(1.3); 291 | transform: scale(1.3); 292 | } 293 | 294 | #todo-list li .destroy:after { 295 | content: '✖'; 296 | } 297 | 298 | #todo-list li:hover .destroy { 299 | display: block; 300 | } 301 | 302 | #todo-list li .edit { 303 | display: none; 304 | } 305 | 306 | #todo-list li.editing:last-child { 307 | margin-bottom: -1px; 308 | } 309 | 310 | #footer { 311 | color: #777; 312 | padding: 0 15px; 313 | position: absolute; 314 | right: 0; 315 | bottom: -31px; 316 | left: 0; 317 | height: 20px; 318 | z-index: 1; 319 | text-align: center; 320 | } 321 | 322 | #footer:before { 323 | content: ''; 324 | position: absolute; 325 | right: 0; 326 | bottom: 31px; 327 | left: 0; 328 | height: 50px; 329 | z-index: -1; 330 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 331 | 0 6px 0 -3px rgba(255, 255, 255, 0.8), 332 | 0 7px 1px -3px rgba(0, 0, 0, 0.3), 333 | 0 43px 0 -6px rgba(255, 255, 255, 0.8), 334 | 0 44px 2px -6px rgba(0, 0, 0, 0.2); 335 | } 336 | 337 | #todo-count { 338 | float: left; 339 | text-align: left; 340 | } 341 | 342 | #filters { 343 | margin: 0; 344 | padding: 0; 345 | list-style: none; 346 | position: absolute; 347 | right: 0; 348 | left: 0; 349 | } 350 | 351 | #filters li { 352 | display: inline; 353 | } 354 | 355 | #filters li a { 356 | color: #83756f; 357 | margin: 2px; 358 | text-decoration: none; 359 | } 360 | 361 | #filters li a.selected { 362 | font-weight: bold; 363 | } 364 | 365 | #clear-completed { 366 | float: right; 367 | position: relative; 368 | line-height: 20px; 369 | text-decoration: none; 370 | background: rgba(0, 0, 0, 0.1); 371 | font-size: 11px; 372 | padding: 0 10px; 373 | border-radius: 3px; 374 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 375 | } 376 | 377 | #clear-completed:hover { 378 | background: rgba(0, 0, 0, 0.15); 379 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 380 | } 381 | 382 | #info { 383 | margin: 65px auto 0; 384 | color: #a6a6a6; 385 | font-size: 12px; 386 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 387 | text-align: center; 388 | } 389 | 390 | #info a { 391 | color: inherit; 392 | } 393 | 394 | /* 395 | Hack to remove background from Mobile Safari. 396 | Can't use it globally since it destroys checkboxes in Firefox and Opera 397 | */ 398 | 399 | @media screen and (-webkit-min-device-pixel-ratio:0) { 400 | #toggle-all, 401 | #todo-list li .toggle { 402 | background: none; 403 | } 404 | 405 | #todo-list li .toggle { 406 | height: 40px; 407 | } 408 | 409 | #toggle-all { 410 | top: -56px; 411 | left: -15px; 412 | width: 65px; 413 | height: 41px; 414 | -webkit-transform: rotate(90deg); 415 | -ms-transform: rotate(90deg); 416 | transform: rotate(90deg); 417 | -webkit-appearance: none; 418 | appearance: none; 419 | } 420 | } 421 | 422 | .hidden { 423 | display: none; 424 | } 425 | 426 | hr { 427 | margin: 20px 0; 428 | border: 0; 429 | border-top: 1px dashed #C5C5C5; 430 | border-bottom: 1px dashed #F7F7F7; 431 | } 432 | 433 | .learn a { 434 | font-weight: normal; 435 | text-decoration: none; 436 | color: #b83f45; 437 | } 438 | 439 | .learn a:hover { 440 | text-decoration: underline; 441 | color: #787e7e; 442 | } 443 | 444 | .learn h3, 445 | .learn h4, 446 | .learn h5 { 447 | margin: 10px 0; 448 | font-weight: 500; 449 | line-height: 1.2; 450 | color: #000; 451 | } 452 | 453 | .learn h3 { 454 | font-size: 24px; 455 | } 456 | 457 | .learn h4 { 458 | font-size: 18px; 459 | } 460 | 461 | .learn h5 { 462 | margin-bottom: 0; 463 | font-size: 14px; 464 | } 465 | 466 | .learn ul { 467 | padding: 0; 468 | margin: 0 0 30px 25px; 469 | } 470 | 471 | .learn li { 472 | line-height: 20px; 473 | } 474 | 475 | .learn p { 476 | font-size: 15px; 477 | font-weight: 300; 478 | line-height: 1.3; 479 | margin-top: 0; 480 | margin-bottom: 0; 481 | } 482 | 483 | .quote { 484 | border: none; 485 | margin: 20px 0 60px 0; 486 | } 487 | 488 | .quote p { 489 | font-style: italic; 490 | } 491 | 492 | .quote p:before { 493 | content: '“'; 494 | font-size: 50px; 495 | opacity: .15; 496 | position: absolute; 497 | top: -20px; 498 | left: 3px; 499 | } 500 | 501 | .quote p:after { 502 | content: '”'; 503 | font-size: 50px; 504 | opacity: .15; 505 | position: absolute; 506 | bottom: -42px; 507 | right: 3px; 508 | } 509 | 510 | .quote footer { 511 | position: absolute; 512 | bottom: -40px; 513 | right: 0; 514 | } 515 | 516 | .quote footer img { 517 | border-radius: 3px; 518 | } 519 | 520 | .quote footer a { 521 | margin-left: 5px; 522 | vertical-align: middle; 523 | } 524 | 525 | .speech-bubble { 526 | position: relative; 527 | padding: 10px; 528 | background: rgba(0, 0, 0, .04); 529 | border-radius: 5px; 530 | } 531 | 532 | .speech-bubble:after { 533 | content: ''; 534 | position: absolute; 535 | top: 100%; 536 | right: 30px; 537 | border: 13px solid transparent; 538 | border-top-color: rgba(0, 0, 0, .04); 539 | } 540 | 541 | .learn-bar > .learn { 542 | position: absolute; 543 | width: 272px; 544 | top: 8px; 545 | left: -300px; 546 | padding: 10px; 547 | border-radius: 5px; 548 | background-color: rgba(255, 255, 255, .6); 549 | -webkit-transition-property: left; 550 | transition-property: left; 551 | -webkit-transition-duration: 500ms; 552 | transition-duration: 500ms; 553 | } 554 | 555 | @media (min-width: 899px) { 556 | .learn-bar { 557 | width: auto; 558 | margin: 0 0 0 300px; 559 | } 560 | 561 | .learn-bar > .learn { 562 | left: 8px; 563 | } 564 | 565 | .learn-bar #todoapp { 566 | width: 550px; 567 | margin: 130px auto 40px auto; 568 | } 569 | } 570 | 571 | /* changes */ 572 | 573 | table, table td, table th { 574 | outline: solid 1px #ccc; 575 | padding: 5px; 576 | text-align: center; 577 | } 578 | 579 | /* debug footer */ 580 | 581 | .debug button { 582 | background: #6c615c; 583 | padding: 5px; 584 | color: #fff; 585 | cursor: pointer; 586 | } 587 | 588 | .debug button:active { 589 | background: #000; 590 | } 591 | 592 | 593 | .debug { 594 | border: dashed 2px #6c615c; 595 | padding: 10px; 596 | } 597 | 598 | .conflicts button { 599 | background: #6c615c; 600 | padding: 5px; 601 | color: #fff; 602 | cursor: pointer; 603 | } 604 | 605 | .conflicts button:active { 606 | background: #000; 607 | } 608 | 609 | .deltas {background: red !important;} 610 | -------------------------------------------------------------------------------- /client/ngapp/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /client/ngapp/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2014-06-23 using 4 | // generator-karma 0.8.2 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | // enable / disable watching file and executing tests whenever any file changes 9 | autoWatch: true, 10 | 11 | // base path, that will be used to resolve files and exclude 12 | basePath: '../', 13 | 14 | // testing framework to use (jasmine/mocha/qunit/...) 15 | frameworks: ['jasmine'], 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | '../../bower_components/es5-shim/es5-shim.js', 20 | '../../bower_components/angular/angular.js', 21 | '../../bower_components/angular-mocks/angular-mocks.js', 22 | '../../bower_components/angular-route/angular-route.js', 23 | '../lbclient/browser.bundle.js', 24 | 'config/bundle.js', 25 | 'scripts/**/*.js', 26 | 'test/mock/**/*.js', 27 | 'test/spec/**/*.js' 28 | ], 29 | 30 | // list of files / patterns to exclude 31 | exclude: [], 32 | 33 | // web server port 34 | port: 8080, 35 | 36 | // Start these browsers, currently available: 37 | // - Chrome 38 | // - ChromeCanary 39 | // - Firefox 40 | // - Opera 41 | // - Safari (only Mac) 42 | // - PhantomJS 43 | // - IE (only Windows) 44 | browsers: [ 45 | 'Chrome' 46 | ], 47 | 48 | // Which plugins to enable 49 | plugins: [ 50 | 'karma-chrome-launcher', 51 | 'karma-phantomjs-launcher', 52 | 'karma-jasmine' 53 | ], 54 | 55 | // Continuous Integration mode 56 | // if true, it capture browsers, run tests and exit 57 | singleRun: false, 58 | 59 | colors: true, 60 | 61 | // level of logging 62 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 63 | logLevel: config.LOG_INFO, 64 | 65 | // Uncomment the following lines if you are using grunt's server to run the tests 66 | // proxies: { 67 | // '/': 'http://localhost:9000/' 68 | // }, 69 | // URL root prevent conflicts with the site root 70 | // urlRoot: '_karma_' 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/change.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: ChangeCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var ChangeCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | ChangeCtrl = $controller('ChangeCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach `clearLocalStorage()` to the scope', function () { 20 | expect(typeof scope.clearLocalStorage).toBe('function'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: HomeCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var HomeCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | HomeCtrl = $controller('HomeCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a `foo` property to the scope', function () { 20 | expect(scope.foo).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: LoginCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var LoginCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | LoginCtrl = $controller('LoginCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a `foo` property to the scope', function () { 20 | expect(scope.foo).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: RegisterCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var RegisterCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | RegisterCtrl = $controller('RegisterCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a `foo` property to the scope', function () { 20 | expect(scope.foo).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/todo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: TodoCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var TodoCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | TodoCtrl = $controller('TodoCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of Todos to the scope', function () { 20 | expect(scope.todos.length).toBe(0); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/controllers/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: UserCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var UserCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | UserCtrl = $controller('UserCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a `foo` property to the scope', function () { 20 | expect(scope.foo).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/ngapp/test/spec/services/lbclient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: lbclient', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | it('should provide Todo model', function() { 9 | inject(function(Todo) { 10 | expect(Todo).toBeDefined(); 11 | }); 12 | }); 13 | 14 | it('should provide `sync()` function', function() { 15 | inject(function(sync) { 16 | expect(typeof sync).toBe('function'); 17 | }); 18 | }); 19 | 20 | it('should provide `network` object', function() { 21 | inject(function(network) { 22 | expect(network).toBeDefined(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/ngapp/views/changes.html: -------------------------------------------------------------------------------- 1 |

Local Change List

2 |

3 | A list of all changes made to models in local storage. 4 |

5 | 6 | No local changes have been made. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Model IDTypeCheckpointRevisionPrev Revision
{{change.modelId}}{{change.type()}}{{change.checkpoint}}{{change.rev}}{{change.prev}}
24 | 25 |

Local to Server Deltas

26 |

27 | Below is list of changes required to replicate local data to the server. 28 |

29 | No changes required to replicate the local data to the server. 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
Model IDRevisionPrev Revision
{{delta.modelId}}{{delta.rev}}{{delta.prev}}
42 | 43 |

Local Storage Data

44 |

45 | Clear Local Storage 46 |

47 | 48 | There is no data in local storage. 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
Todo IDTitleCompleted
{{todo.getId()}}{{todo.title}}{{todo.completed}}
62 | 63 |

Local to Server Conflicts

64 |

65 | Below is list of changes that cannot be replicated to the server. 66 |

67 | No conflicts... 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
Model IDRevisionPrev Revision
{{conflict.modelId}}{{conflict.rev}}{{conflict.prev}}
80 | -------------------------------------------------------------------------------- /client/ngapp/views/login.html: -------------------------------------------------------------------------------- 1 |

Login

2 | 3 |

Under construction.

4 | -------------------------------------------------------------------------------- /client/ngapp/views/register.html: -------------------------------------------------------------------------------- 1 |

Register

2 | 3 |

Under construction.

4 | -------------------------------------------------------------------------------- /client/ngapp/views/todos.html: -------------------------------------------------------------------------------- 1 |
2 |

Local Conflicts

3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 30 | 50 | 51 |
Local DataRemote Data
11 |
12 | Deleted 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 |
idchangetitle
{{conflict.sourceChange.modelId}}{{conflict.sourceChange.type()}} 24 | {{conflict.source.title}} 25 |
28 | 29 |
31 |
32 | Deleted 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 |
idchangetitle
{{conflict.targetChange.modelId}}{{conflict.targetChange.type()}} 44 | {{conflict.target.title}} 45 |
48 | 49 |
52 |
53 |

Merge Manually

54 | 55 | 56 | 57 |
58 |
59 |
60 | 61 |
62 | 68 |
69 | 70 | 71 |
    72 |
  • 73 |
    74 | 75 | 76 | 77 |
    78 |
    79 | 80 |
    81 |
  • 82 |
83 |
84 |
85 | {{remainingCount}} 86 | 87 | 88 | 99 | 100 |
101 |
102 | 105 | 112 | 113 | -------------------------------------------------------------------------------- /client/ngapp/views/user.html: -------------------------------------------------------------------------------- 1 |

User

2 | 3 |

Under construction.

4 | -------------------------------------------------------------------------------- /client/ngapp/views/welcome.html: -------------------------------------------------------------------------------- 1 |

Welcome

2 | 3 | This is the welcome view. 4 | 5 | View your todo list. 6 | -------------------------------------------------------------------------------- /client/reapp/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /client/reapp/config/bundle.js: -------------------------------------------------------------------------------- 1 | window.CONFIG = { 2 | "routes": { 3 | "/": { 4 | "name": "home", 5 | "handler": "WelcomeView", 6 | "default": true 7 | }, 8 | "/me": { 9 | "name": "user", 10 | "handler": "UserView" 11 | }, 12 | "/my/todos/:status": { 13 | "name": "todoStatus", 14 | "handler": "TodoView" 15 | }, 16 | "/my/todos": { 17 | "name": "todos", 18 | "handler": "TodoView" 19 | }, 20 | "/login": { 21 | "name": "login", 22 | "handler": "LoginView" 23 | }, 24 | "/register": { 25 | "name": "register", 26 | "handler": "RegisterView" 27 | }, 28 | "/debug": { 29 | "name": "debug", 30 | "handler": "ChangeView" 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /client/reapp/config/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": { 3 | "name": "home", 4 | "handler": "WelcomeView", 5 | "default": true 6 | }, 7 | "/me": { 8 | "name": "user", 9 | "handler": "UserView" 10 | }, 11 | "/my/todos/:status": { 12 | "name": "todoStatus", 13 | "handler": "TodoView" 14 | }, 15 | "/my/todos": { 16 | "name": "todos", 17 | "handler": "TodoView" 18 | }, 19 | "/login": { 20 | "name": "login", 21 | "handler": "LoginView" 22 | }, 23 | "/register": { 24 | "name": "register", 25 | "handler": "RegisterView" 26 | }, 27 | "/debug": { 28 | "name": "debug", 29 | "handler": "ChangeView" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/reapp/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dundalek/loopback-example-full-stack-react/f5d63172ee1e3e19553dc1a879b9a541c6f17730/client/reapp/favicon.ico -------------------------------------------------------------------------------- /client/reapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo App 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 |
32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /client/reapp/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /client/reapp/scripts/actions.js: -------------------------------------------------------------------------------- 1 | (typeof exports !== 'undefined' ? exports : window).Actions = Reflux.createActions([ 2 | 'addTodo', 3 | 'removeTodo', 4 | 'todoEdited', 5 | 'toggleCompleted', 6 | 'markAll', 7 | 'clearCompletedTodos', 8 | 9 | 'sync', 10 | 'connect', 11 | 'disconnect', 12 | 'resolveUsingSource', 13 | 'resolveUsingTarget', 14 | 'resolveManually', 15 | 16 | 'clearLocalStorage' 17 | ]); 18 | -------------------------------------------------------------------------------- /client/reapp/scripts/app.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Router = ReactRouter; 4 | var Route = Router.Route; 5 | var Link = Router.Link; 6 | var RouteHandler = Router.RouteHandler; 7 | var DefaultRoute = Router.DefaultRoute; 8 | 9 | var App = React.createClass({ 10 | render: function() { 11 | return
12 |

Welcome to the Todo App

13 | 14 |
15 |
    16 |
  • Home
  • 17 |
  • Login
  • 18 |
  • Register
  • 19 |
20 |
21 | 22 | 23 |
24 | } 25 | }); 26 | 27 | var routes = ( 28 | 29 | {Object.keys(window.CONFIG.routes) 30 | .map(function(route) { 31 | var routeDef = window.CONFIG.routes[route]; 32 | var R = routeDef.default ? DefaultRoute : Route; 33 | return 34 | })} 35 | 36 | ); 37 | 38 | Router.run(routes, Router.HistoryLocation, function (Handler) { 39 | React.render(, document.getElementById('loopbackExampleFullStackApp')); 40 | }); 41 | -------------------------------------------------------------------------------- /client/reapp/scripts/services/lbclient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // load lbclient via browserify's require 4 | var client = (function() { 5 | /*global require:true*/ 6 | return require('lbclient'); 7 | })(); 8 | 9 | var Todo = client.models.LocalTodo; 10 | var RemoteTodo = client.models.Todo; 11 | var sync = client.sync; 12 | var network = client.network; 13 | -------------------------------------------------------------------------------- /client/reapp/scripts/stores/change-store.js: -------------------------------------------------------------------------------- 1 | (typeof exports !== 'undefined' ? exports : window).ChangeStore = function() { 2 | 3 | var $scope; 4 | 5 | return Reflux.createStore({ 6 | listenables: Actions, 7 | 8 | init: function() { 9 | $scope = this; 10 | 11 | this.changes = []; 12 | this.todos = []; 13 | this.diff = { 14 | deltas: [], 15 | conflicts: [] 16 | } 17 | 18 | Todo.getChangeModel().find(function(err, changes) { 19 | $scope.changes = changes; 20 | $scope.trigger(); 21 | 22 | RemoteTodo.diff(0, changes, function(err, diff) { 23 | $scope.diff = diff; 24 | $scope.trigger(); 25 | }); 26 | }); 27 | 28 | Todo.find(function(err, todos) { 29 | $scope.todos = todos; 30 | $scope.trigger(); 31 | }); 32 | }, 33 | 34 | onClearLocalStorage: function() { 35 | localStorage.removeItem('todo-db'); 36 | } 37 | 38 | }); 39 | }(); 40 | -------------------------------------------------------------------------------- /client/reapp/scripts/stores/todo-store.js: -------------------------------------------------------------------------------- 1 | (typeof exports !== 'undefined' ? exports : window).TodoStore = function() { 2 | 3 | var $scope; 4 | 5 | function error(err) { 6 | //TODO error handling 7 | throw err; 8 | } 9 | 10 | function errorCallback(err) { 11 | if(err) error(err); 12 | } 13 | 14 | function onChange() { 15 | Todo.stats(function(err, stats) { 16 | if(err) return error(err); 17 | $scope.stats = stats; 18 | }); 19 | Todo.find({ 20 | where: $scope.statusFilter, 21 | sort: 'order DESC' 22 | }, function(err, todos) { 23 | $scope.todos = todos; 24 | $scope.trigger(); 25 | }); 26 | } 27 | 28 | function refreshConflicts() { 29 | $scope.localConflicts = []; 30 | $scope.trigger(); 31 | sync(); 32 | } 33 | 34 | return Reflux.createStore({ 35 | listenables: Actions, 36 | 37 | init: function() { 38 | $scope = this; 39 | 40 | this.todos = []; 41 | this.stats = []; 42 | this.localConflicts = []; 43 | 44 | // sync the initial data 45 | sync(onChange); 46 | 47 | Todo.on('changed', onChange); 48 | Todo.on('deleted', onChange); 49 | 50 | Todo.on('conflicts', function(conflicts) { 51 | $scope.localConflicts = conflicts; 52 | conflicts.forEach(function(conflict) { 53 | conflict.type(function(err, type) { 54 | conflict.type = type; 55 | conflict.models(function(err, source, target) { 56 | conflict.source = source; 57 | conflict.target = target; 58 | conflict.manual = new conflict.SourceModel(source || target); 59 | conflict.changes(function(err, source, target) { 60 | conflict.sourceChange = source; 61 | conflict.targetChange = target; 62 | $scope.trigger(); 63 | }); 64 | }); 65 | }); 66 | }); 67 | }); 68 | }, 69 | 70 | onAddTodo: function(title) { 71 | var todo = new Todo({title: title}); 72 | todo.save(); 73 | }, 74 | 75 | onToggleCompleted: function(todo, completed) { 76 | todo.completed = completed; 77 | todo.save(); 78 | }, 79 | 80 | onTodoEdited: function (todo, title) { 81 | todo.title = title.trim(); 82 | 83 | if (!todo.title) { 84 | this.onRemoveTodo(todo); 85 | } else { 86 | todo.save(); 87 | } 88 | }, 89 | 90 | onRemoveTodo: function (todo) { 91 | todo.remove(errorCallback); 92 | }, 93 | 94 | onClearCompletedTodos: function () { 95 | Todo.destroyAll({completed: true}, onChange); 96 | }, 97 | 98 | onMarkAll: function (completed) { 99 | Todo.find(function(err, todos) { 100 | if(err) return errorCallback(err); 101 | todos.forEach(function(todo) { 102 | todo.completed = completed; 103 | todo.save(errorCallback); 104 | }); 105 | }); 106 | }, 107 | 108 | connected: function() { 109 | return network.isConnected; 110 | }, 111 | 112 | onSync: function() { 113 | sync(); 114 | }, 115 | 116 | onConnect: function() { 117 | network.isConnected = true; 118 | this.trigger(); 119 | sync(); 120 | }, 121 | 122 | onDisconnect: function() { 123 | network.isConnected = false; 124 | this.trigger(); 125 | }, 126 | 127 | onResolveUsingSource: function(conflict) { 128 | conflict.resolve(refreshConflicts); 129 | }, 130 | 131 | onResolveUsingTarget: function(conflict) { 132 | if(conflict.targetChange.type() === 'delete') { 133 | conflict.SourceModel.deleteById(conflict.modelId, refreshConflicts); 134 | } else { 135 | var m = new conflict.SourceModel(conflict.target); 136 | m.save(refreshConflicts); 137 | } 138 | }, 139 | 140 | onResolveManually: function(conflict) { 141 | conflict.manual.save(function(err) { 142 | if(err) return errorCallback(err); 143 | conflict.resolve(refreshConflicts); 144 | }); 145 | } 146 | 147 | }); 148 | }(); 149 | -------------------------------------------------------------------------------- /client/reapp/styles/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | color: inherit; 16 | -webkit-appearance: none; 17 | -ms-appearance: none; 18 | -o-appearance: none; 19 | appearance: none; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #eaeaea; 26 | color: #4d4d4d; 27 | width: 550px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -ms-font-smoothing: antialiased; 32 | -o-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | } 35 | 36 | .header { 37 | border-bottom: 1px solid #e5e5e5; 38 | } 39 | 40 | .header .navigation { 41 | padding-left: 0; 42 | list-style-type: none; 43 | } 44 | 45 | .header .navigation li { 46 | display: inline; 47 | } 48 | 49 | button, 50 | input[type="checkbox"] { 51 | outline: none; 52 | } 53 | 54 | #todoapp { 55 | background: #fff; 56 | background: rgba(255, 255, 255, 0.9); 57 | margin: 130px 0 40px 0; 58 | border: 1px solid #ccc; 59 | position: relative; 60 | border-top-left-radius: 2px; 61 | border-top-right-radius: 2px; 62 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 63 | 0 25px 50px 0 rgba(0, 0, 0, 0.15); 64 | } 65 | 66 | #todoapp:before { 67 | content: ''; 68 | border-left: 1px solid #f5d6d6; 69 | border-right: 1px solid #f5d6d6; 70 | width: 2px; 71 | position: absolute; 72 | top: 0; 73 | left: 40px; 74 | height: 100%; 75 | } 76 | 77 | #todoapp input::-webkit-input-placeholder { 78 | font-style: italic; 79 | } 80 | 81 | #todoapp input::-moz-placeholder { 82 | font-style: italic; 83 | color: #a9a9a9; 84 | } 85 | 86 | #todoapp h1 { 87 | position: absolute; 88 | top: -120px; 89 | width: 100%; 90 | font-size: 70px; 91 | font-weight: bold; 92 | text-align: center; 93 | color: #b3b3b3; 94 | color: rgba(255, 255, 255, 0.3); 95 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 96 | -webkit-text-rendering: optimizeLegibility; 97 | -moz-text-rendering: optimizeLegibility; 98 | -ms-text-rendering: optimizeLegibility; 99 | -o-text-rendering: optimizeLegibility; 100 | text-rendering: optimizeLegibility; 101 | } 102 | 103 | #header { 104 | padding-top: 15px; 105 | border-radius: inherit; 106 | } 107 | 108 | #header:before { 109 | content: ''; 110 | position: absolute; 111 | top: 0; 112 | right: 0; 113 | left: 0; 114 | height: 15px; 115 | z-index: 2; 116 | border-bottom: 1px solid #6c615c; 117 | background: #8d7d77; 118 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 119 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 120 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 121 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 122 | border-top-left-radius: 1px; 123 | border-top-right-radius: 1px; 124 | } 125 | 126 | #new-todo, 127 | .edit { 128 | position: relative; 129 | margin: 0; 130 | width: 100%; 131 | font-size: 24px; 132 | font-family: inherit; 133 | line-height: 1.4em; 134 | border: 0; 135 | outline: none; 136 | color: inherit; 137 | padding: 6px; 138 | border: 1px solid #999; 139 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 140 | -moz-box-sizing: border-box; 141 | -ms-box-sizing: border-box; 142 | -o-box-sizing: border-box; 143 | box-sizing: border-box; 144 | -webkit-font-smoothing: antialiased; 145 | -moz-font-smoothing: antialiased; 146 | -ms-font-smoothing: antialiased; 147 | -o-font-smoothing: antialiased; 148 | font-smoothing: antialiased; 149 | } 150 | 151 | #new-todo { 152 | padding: 16px 16px 16px 60px; 153 | border: none; 154 | background: rgba(0, 0, 0, 0.02); 155 | z-index: 2; 156 | box-shadow: none; 157 | } 158 | 159 | #main { 160 | position: relative; 161 | z-index: 2; 162 | border-top: 1px dotted #adadad; 163 | } 164 | 165 | label[for='toggle-all'] { 166 | display: none; 167 | } 168 | 169 | #toggle-all { 170 | position: absolute; 171 | top: -42px; 172 | left: -4px; 173 | width: 40px; 174 | text-align: center; 175 | /* Mobile Safari */ 176 | border: none; 177 | } 178 | 179 | #toggle-all:before { 180 | content: '»'; 181 | font-size: 28px; 182 | color: #d9d9d9; 183 | padding: 0 25px 7px; 184 | } 185 | 186 | #toggle-all:checked:before { 187 | color: #737373; 188 | } 189 | 190 | #todo-list { 191 | margin: 0; 192 | padding: 0; 193 | list-style: none; 194 | } 195 | 196 | #todo-list li { 197 | position: relative; 198 | font-size: 24px; 199 | border-bottom: 1px dotted #ccc; 200 | } 201 | 202 | #todo-list li:last-child { 203 | border-bottom: none; 204 | } 205 | 206 | #todo-list li.editing { 207 | border-bottom: none; 208 | padding: 0; 209 | } 210 | 211 | #todo-list li.editing .edit { 212 | display: block; 213 | width: 506px; 214 | padding: 13px 17px 12px 17px; 215 | margin: 0 0 0 43px; 216 | } 217 | 218 | #todo-list li.editing .view { 219 | display: none; 220 | } 221 | 222 | #todo-list li .toggle { 223 | text-align: center; 224 | width: 40px; 225 | /* auto, since non-WebKit browsers doesn't support input styling */ 226 | height: auto; 227 | position: absolute; 228 | top: 0; 229 | bottom: 0; 230 | margin: auto 0; 231 | /* Mobile Safari */ 232 | border: none; 233 | -webkit-appearance: none; 234 | -ms-appearance: none; 235 | -o-appearance: none; 236 | appearance: none; 237 | } 238 | 239 | #todo-list li .toggle:after { 240 | content: '✔'; 241 | /* 40 + a couple of pixels visual adjustment */ 242 | line-height: 43px; 243 | font-size: 20px; 244 | color: #d9d9d9; 245 | text-shadow: 0 -1px 0 #bfbfbf; 246 | } 247 | 248 | #todo-list li .toggle:checked:after { 249 | color: #85ada7; 250 | text-shadow: 0 1px 0 #669991; 251 | bottom: 1px; 252 | position: relative; 253 | } 254 | 255 | #todo-list li label { 256 | white-space: pre; 257 | word-break: break-word; 258 | padding: 15px 60px 15px 15px; 259 | margin-left: 45px; 260 | display: block; 261 | line-height: 1.2; 262 | -webkit-transition: color 0.4s; 263 | transition: color 0.4s; 264 | } 265 | 266 | #todo-list li.completed label { 267 | color: #a9a9a9; 268 | text-decoration: line-through; 269 | } 270 | 271 | #todo-list li .destroy { 272 | display: none; 273 | position: absolute; 274 | top: 0; 275 | right: 10px; 276 | bottom: 0; 277 | width: 40px; 278 | height: 40px; 279 | margin: auto 0; 280 | font-size: 22px; 281 | color: #a88a8a; 282 | -webkit-transition: all 0.2s; 283 | transition: all 0.2s; 284 | } 285 | 286 | #todo-list li .destroy:hover { 287 | text-shadow: 0 0 1px #000, 288 | 0 0 10px rgba(199, 107, 107, 0.8); 289 | -webkit-transform: scale(1.3); 290 | -ms-transform: scale(1.3); 291 | transform: scale(1.3); 292 | } 293 | 294 | #todo-list li .destroy:after { 295 | content: '✖'; 296 | } 297 | 298 | #todo-list li:hover .destroy { 299 | display: block; 300 | } 301 | 302 | #todo-list li .edit { 303 | display: none; 304 | } 305 | 306 | #todo-list li.editing:last-child { 307 | margin-bottom: -1px; 308 | } 309 | 310 | #footer { 311 | color: #777; 312 | padding: 0 15px; 313 | position: absolute; 314 | right: 0; 315 | bottom: -31px; 316 | left: 0; 317 | height: 20px; 318 | z-index: 1; 319 | text-align: center; 320 | } 321 | 322 | #footer:before { 323 | content: ''; 324 | position: absolute; 325 | right: 0; 326 | bottom: 31px; 327 | left: 0; 328 | height: 50px; 329 | z-index: -1; 330 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 331 | 0 6px 0 -3px rgba(255, 255, 255, 0.8), 332 | 0 7px 1px -3px rgba(0, 0, 0, 0.3), 333 | 0 43px 0 -6px rgba(255, 255, 255, 0.8), 334 | 0 44px 2px -6px rgba(0, 0, 0, 0.2); 335 | } 336 | 337 | #todo-count { 338 | float: left; 339 | text-align: left; 340 | } 341 | 342 | #filters { 343 | margin: 0; 344 | padding: 0; 345 | list-style: none; 346 | position: absolute; 347 | right: 0; 348 | left: 0; 349 | } 350 | 351 | #filters li { 352 | display: inline; 353 | } 354 | 355 | #filters li a { 356 | color: #83756f; 357 | margin: 2px; 358 | text-decoration: none; 359 | } 360 | 361 | #filters li a.selected { 362 | font-weight: bold; 363 | } 364 | 365 | #clear-completed { 366 | float: right; 367 | position: relative; 368 | line-height: 20px; 369 | text-decoration: none; 370 | background: rgba(0, 0, 0, 0.1); 371 | font-size: 11px; 372 | padding: 0 10px; 373 | border-radius: 3px; 374 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 375 | } 376 | 377 | #clear-completed:hover { 378 | background: rgba(0, 0, 0, 0.15); 379 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 380 | } 381 | 382 | #info { 383 | margin: 65px auto 0; 384 | color: #a6a6a6; 385 | font-size: 12px; 386 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 387 | text-align: center; 388 | } 389 | 390 | #info a { 391 | color: inherit; 392 | } 393 | 394 | /* 395 | Hack to remove background from Mobile Safari. 396 | Can't use it globally since it destroys checkboxes in Firefox and Opera 397 | */ 398 | 399 | @media screen and (-webkit-min-device-pixel-ratio:0) { 400 | #toggle-all, 401 | #todo-list li .toggle { 402 | background: none; 403 | } 404 | 405 | #todo-list li .toggle { 406 | height: 40px; 407 | } 408 | 409 | #toggle-all { 410 | top: -56px; 411 | left: -15px; 412 | width: 65px; 413 | height: 41px; 414 | -webkit-transform: rotate(90deg); 415 | -ms-transform: rotate(90deg); 416 | transform: rotate(90deg); 417 | -webkit-appearance: none; 418 | appearance: none; 419 | } 420 | } 421 | 422 | .hidden { 423 | display: none; 424 | } 425 | 426 | hr { 427 | margin: 20px 0; 428 | border: 0; 429 | border-top: 1px dashed #C5C5C5; 430 | border-bottom: 1px dashed #F7F7F7; 431 | } 432 | 433 | .learn a { 434 | font-weight: normal; 435 | text-decoration: none; 436 | color: #b83f45; 437 | } 438 | 439 | .learn a:hover { 440 | text-decoration: underline; 441 | color: #787e7e; 442 | } 443 | 444 | .learn h3, 445 | .learn h4, 446 | .learn h5 { 447 | margin: 10px 0; 448 | font-weight: 500; 449 | line-height: 1.2; 450 | color: #000; 451 | } 452 | 453 | .learn h3 { 454 | font-size: 24px; 455 | } 456 | 457 | .learn h4 { 458 | font-size: 18px; 459 | } 460 | 461 | .learn h5 { 462 | margin-bottom: 0; 463 | font-size: 14px; 464 | } 465 | 466 | .learn ul { 467 | padding: 0; 468 | margin: 0 0 30px 25px; 469 | } 470 | 471 | .learn li { 472 | line-height: 20px; 473 | } 474 | 475 | .learn p { 476 | font-size: 15px; 477 | font-weight: 300; 478 | line-height: 1.3; 479 | margin-top: 0; 480 | margin-bottom: 0; 481 | } 482 | 483 | .quote { 484 | border: none; 485 | margin: 20px 0 60px 0; 486 | } 487 | 488 | .quote p { 489 | font-style: italic; 490 | } 491 | 492 | .quote p:before { 493 | content: '“'; 494 | font-size: 50px; 495 | opacity: .15; 496 | position: absolute; 497 | top: -20px; 498 | left: 3px; 499 | } 500 | 501 | .quote p:after { 502 | content: '”'; 503 | font-size: 50px; 504 | opacity: .15; 505 | position: absolute; 506 | bottom: -42px; 507 | right: 3px; 508 | } 509 | 510 | .quote footer { 511 | position: absolute; 512 | bottom: -40px; 513 | right: 0; 514 | } 515 | 516 | .quote footer img { 517 | border-radius: 3px; 518 | } 519 | 520 | .quote footer a { 521 | margin-left: 5px; 522 | vertical-align: middle; 523 | } 524 | 525 | .speech-bubble { 526 | position: relative; 527 | padding: 10px; 528 | background: rgba(0, 0, 0, .04); 529 | border-radius: 5px; 530 | } 531 | 532 | .speech-bubble:after { 533 | content: ''; 534 | position: absolute; 535 | top: 100%; 536 | right: 30px; 537 | border: 13px solid transparent; 538 | border-top-color: rgba(0, 0, 0, .04); 539 | } 540 | 541 | .learn-bar > .learn { 542 | position: absolute; 543 | width: 272px; 544 | top: 8px; 545 | left: -300px; 546 | padding: 10px; 547 | border-radius: 5px; 548 | background-color: rgba(255, 255, 255, .6); 549 | -webkit-transition-property: left; 550 | transition-property: left; 551 | -webkit-transition-duration: 500ms; 552 | transition-duration: 500ms; 553 | } 554 | 555 | @media (min-width: 899px) { 556 | .learn-bar { 557 | width: auto; 558 | margin: 0 0 0 300px; 559 | } 560 | 561 | .learn-bar > .learn { 562 | left: 8px; 563 | } 564 | 565 | .learn-bar #todoapp { 566 | width: 550px; 567 | margin: 130px auto 40px auto; 568 | } 569 | } 570 | 571 | /* changes */ 572 | 573 | table, table td, table th { 574 | outline: solid 1px #ccc; 575 | padding: 5px; 576 | text-align: center; 577 | } 578 | 579 | /* debug footer */ 580 | 581 | .debug button { 582 | background: #6c615c; 583 | padding: 5px; 584 | color: #fff; 585 | cursor: pointer; 586 | } 587 | 588 | .debug button:active { 589 | background: #000; 590 | } 591 | 592 | 593 | .debug { 594 | border: dashed 2px #6c615c; 595 | padding: 10px; 596 | } 597 | 598 | .conflicts button { 599 | background: #6c615c; 600 | padding: 5px; 601 | color: #fff; 602 | cursor: pointer; 603 | } 604 | 605 | .conflicts button:active { 606 | background: #000; 607 | } 608 | 609 | .deltas {background: red !important;} 610 | -------------------------------------------------------------------------------- /client/reapp/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "jasmine": false, 33 | "spyOn": false 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /client/reapp/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2014-06-23 using 4 | // generator-karma 0.8.2 5 | 6 | module.exports = function(config) { 7 | config.set({ 8 | // enable / disable watching file and executing tests whenever any file changes 9 | autoWatch: true, 10 | 11 | // base path, that will be used to resolve files and exclude 12 | basePath: '../', 13 | 14 | // testing framework to use (jasmine/mocha/qunit/...) 15 | frameworks: ['jasmine'], 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | '../../bower_components/es5-shim/es5-shim.js', 20 | '../../bower_components/angular/angular.js', 21 | '../../bower_components/angular-mocks/angular-mocks.js', 22 | '../../bower_components/angular-route/angular-route.js', 23 | '../lbclient/browser.bundle.js', 24 | 'config/bundle.js', 25 | 'scripts/**/*.js', 26 | 'test/mock/**/*.js', 27 | 'test/spec/**/*.js' 28 | ], 29 | 30 | // list of files / patterns to exclude 31 | exclude: [], 32 | 33 | // web server port 34 | port: 8080, 35 | 36 | // Start these browsers, currently available: 37 | // - Chrome 38 | // - ChromeCanary 39 | // - Firefox 40 | // - Opera 41 | // - Safari (only Mac) 42 | // - PhantomJS 43 | // - IE (only Windows) 44 | browsers: [ 45 | 'Chrome' 46 | ], 47 | 48 | // Which plugins to enable 49 | plugins: [ 50 | 'karma-chrome-launcher', 51 | 'karma-phantomjs-launcher', 52 | 'karma-jasmine' 53 | ], 54 | 55 | // Continuous Integration mode 56 | // if true, it capture browsers, run tests and exit 57 | singleRun: false, 58 | 59 | colors: true, 60 | 61 | // level of logging 62 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 63 | logLevel: config.LOG_INFO, 64 | 65 | // Uncomment the following lines if you are using grunt's server to run the tests 66 | // proxies: { 67 | // '/': 'http://localhost:9000/' 68 | // }, 69 | // URL root prevent conflicts with the site root 70 | // urlRoot: '_karma_' 71 | }); 72 | }; 73 | -------------------------------------------------------------------------------- /client/reapp/test/spec/controllers/change.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: ChangeCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var ChangeCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | ChangeCtrl = $controller('ChangeCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach `clearLocalStorage()` to the scope', function () { 20 | expect(typeof scope.clearLocalStorage).toBe('function'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/reapp/test/spec/controllers/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: HomeCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var HomeCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | HomeCtrl = $controller('HomeCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a `foo` property to the scope', function () { 20 | expect(scope.foo).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/reapp/test/spec/controllers/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: LoginCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var LoginCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | LoginCtrl = $controller('LoginCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a `foo` property to the scope', function () { 20 | expect(scope.foo).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/reapp/test/spec/controllers/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: RegisterCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var RegisterCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | RegisterCtrl = $controller('RegisterCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a `foo` property to the scope', function () { 20 | expect(scope.foo).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/reapp/test/spec/controllers/todo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: TodoCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var TodoCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | TodoCtrl = $controller('TodoCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a list of Todos to the scope', function () { 20 | expect(scope.todos.length).toBe(0); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/reapp/test/spec/controllers/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: UserCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | var UserCtrl, 9 | scope; 10 | 11 | // Initialize the controller and a mock scope 12 | beforeEach(inject(function ($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | UserCtrl = $controller('UserCtrl', { 15 | $scope: scope 16 | }); 17 | })); 18 | 19 | it('should attach a `foo` property to the scope', function () { 20 | expect(scope.foo).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /client/reapp/test/spec/services/lbclient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Service: lbclient', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('loopbackExampleFullStackApp')); 7 | 8 | it('should provide Todo model', function() { 9 | inject(function(Todo) { 10 | expect(Todo).toBeDefined(); 11 | }); 12 | }); 13 | 14 | it('should provide `sync()` function', function() { 15 | inject(function(sync) { 16 | expect(typeof sync).toBe('function'); 17 | }); 18 | }); 19 | 20 | it('should provide `network` object', function() { 21 | inject(function(network) { 22 | expect(network).toBeDefined(); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /client/reapp/views/changes.jsx: -------------------------------------------------------------------------------- 1 | (typeof exports !== 'undefined' ? exports : window).ChangeView = React.createClass({ 2 | mixins: [ 3 | Reflux.listenTo(TodoStore, 'update') 4 | ], 5 | render: function() { 6 | var changes = ChangeStore.changes; 7 | var todos = ChangeStore.todos; 8 | var diff = ChangeStore.diff; 9 | 10 | return
11 |

Local Change List

12 |

13 | A list of all changes made to models in local storage. 14 |

15 | {changes.length === 0 && 16 | No local changes have been made. 17 | } 18 | {changes.length > 0 && 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {changes.map(function(change) { 27 | return 28 | 29 | 30 | 31 | 32 | 33 | 34 | })} 35 |
Model IDTypeCheckpointRevisionPrev Revision
{change.modelId}{change.type()}{change.checkpoint}{change.rev}{change.prev}
} 36 | 37 |

Local to Server Deltas

38 |

39 | Below is list of changes required to replicate local data to the server. 40 |

41 | {diff.deltas.length === 0 && No changes required to replicate the local data to the server.} 42 | {diff.deltas.length > 0 && 43 | 44 | 45 | 46 | 47 | 48 | {diff.deltas.map(function(delta) { 49 | return 50 | 51 | 52 | 53 | 54 | })} 55 |
Model IDRevisionPrev Revision
{delta.modelId}{delta.rev}{delta.prev}
} 56 | 57 |

Local Storage Data

58 |

59 | Clear Local Storage 60 |

61 | {todos.length === 0 && 62 | There is no data in local storage. 63 | } 64 | {todos.length > 0 && 65 | 66 | 67 | 68 | 69 | 70 | {todos.map(function(todo) { 71 | return 72 | 73 | 74 | 75 | 76 | })} 77 |
Todo IDTitleCompleted
{todo.getId()}{todo.title}{todo.completed}
} 78 | 79 |

Local to Server Conflicts

80 |

81 | Below is list of changes that cannot be replicated to the server. 82 |

83 | {diff.conflicts.length === 0 && No conflicts...} 84 | {diff.conflicts.length > 0 && 85 | 86 | 87 | 88 | 89 | 90 | {diff.conflicts.map(function(conflict) { 91 | return 92 | 93 | 94 | 95 | 96 | })} 97 |
Model IDRevisionPrev Revision
{conflict.modelId}{conflict.rev}{conflict.prev}
} 98 | 99 |
100 | }, 101 | update: function() { 102 | this.forceUpdate(); 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /client/reapp/views/login.jsx: -------------------------------------------------------------------------------- 1 | (typeof exports !== 'undefined' ? exports : window).LoginView = React.createClass({ 2 | render: function() { 3 | return
4 | 5 |

Login

6 | 7 |

Under construction.

8 | 9 |
10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /client/reapp/views/register.jsx: -------------------------------------------------------------------------------- 1 | (typeof exports !== 'undefined' ? exports : window).RegisterView = React.createClass({ 2 | render: function() { 3 | return
4 | 5 |

Register

6 | 7 |

Under construction.

8 | 9 |
10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /client/reapp/views/todos.jsx: -------------------------------------------------------------------------------- 1 | (typeof exports !== 'undefined' ? exports : window).TodoView = React.createClass({ 2 | mixins: [ 3 | Reflux.listenTo(TodoStore, 'update'), 4 | React.addons.LinkedStateMixin, 5 | ReactRouter.State 6 | ], 7 | getInitialState: function() { 8 | return { 9 | newTodo: '', 10 | editedTodo: null 11 | }; 12 | }, 13 | render: function() { 14 | var todos = TodoStore.todos; 15 | var stats = TodoStore.stats; 16 | var allChecked = todos.length === stats.completed; 17 | var status = this.getParams().status; 18 | var visibleTodos = todos.filter(function(todo) { 19 | return !status 20 | || (status === 'active' && !todo.completed) 21 | || (status === 'completed' && todo.completed); 22 | }); 23 | var localConflicts = TodoStore.localConflicts; 24 | 25 | return
26 | 27 | {localConflicts.length > 0 &&
28 |

Local Conflicts

29 | {localConflicts.map(function(conflict) { 30 | var sourceChangeType = conflict.sourceChange && conflict.sourceChange.type && conflict.sourceChange.type(); 31 | var targetChangeType = conflict.targetChange && conflict.targetChange.type && conflict.targetChange.type(); 32 | 33 | return
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 61 | 81 | 82 |
Local DataRemote Data
42 | {sourceChangeType === 'delete' &&
43 | Deleted 44 |
} 45 | {sourceChangeType !== 'delete' && 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 |
idchangetitle
{conflict.sourceChange.modelId}{sourceChangeType} 55 | {conflict.source && conflict.source.title} 56 |
} 59 | 60 |
62 | {targetChangeType === 'delete' &&
63 | Deleted 64 |
} 65 | {targetChangeType !== 'delete' && 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 77 | 78 |
idchangetitle
{conflict.targetChange.modelId}{targetChangeType} 75 | {conflict.target && conflict.target.title} 76 |
} 79 | 80 |
83 | 84 | 85 |
86 | })} 87 |
} 88 | 89 |
90 | 96 |
97 | 98 | 99 |
    100 | {visibleTodos.map(function(todo) { 101 | return 102 | }.bind(this))} 103 |
104 |
105 |
106 | {stats.remaining} {stats.remaining === 1 ? 'item left' : 'items left'} 107 | 108 |
    109 |
  • 110 | All 111 |
  • 112 |
  • 113 | Active 114 |
  • 115 |
  • 116 | Completed 117 |
  • 118 |
119 | 120 |
121 |
122 |
123 |

Double-click to edit a todo

124 |
125 |
126 | {' '} 127 | {' '} 128 | {' '} 129 | Debug {' '} 130 | connected: {String(TodoStore.connected())} 131 |
132 | 133 |
134 | }, 135 | update: function() { 136 | this.forceUpdate(); 137 | }, 138 | addTodo: function(ev) { 139 | ev.preventDefault(); 140 | Actions.addTodo(this.state.newTodo); 141 | this.setState({ 142 | newTodo: '' 143 | }); 144 | }, 145 | markAll: function(ev) { 146 | var checked = ev.target.checked; 147 | Actions.markAll(checked); 148 | } 149 | }); 150 | 151 | 152 | var TodoItem = React.createClass({ 153 | getInitialState: function() { 154 | return { 155 | editing: false, 156 | newValue: null 157 | }; 158 | }, 159 | render: function() { 160 | var todo = this.props.todo; 161 | var title = this.state.newValue !== null ? this.state.newValue : todo.title; 162 | 163 | return
  • 164 |
    165 | 166 | 167 | 168 |
    169 | {this.state.editing &&
    170 | 171 |
    } 172 |
  • 173 | }, 174 | editTodo: function() { 175 | this.setState({ 176 | editing: true 177 | }); 178 | }, 179 | changeTodo: function(ev) { 180 | this.setState({ 181 | newValue: ev.target.value 182 | }); 183 | }, 184 | removeTodo: function() { 185 | this.props.onRemoveTodo(this.props.todo); 186 | }, 187 | toggleCompleted: function(ev) { 188 | this.props.onToggleCompleted(this.props.todo, ev.target.checked); 189 | }, 190 | todoEdited: function(ev) { 191 | ev.preventDefault(); 192 | if (this.state.newValue !== null) { 193 | this.props.onTodoEdited(this.props.todo, this.state.newValue); 194 | } 195 | this.setState({ 196 | newValue: null, 197 | editing: false 198 | }); 199 | } 200 | }); 201 | 202 | 203 | var ManualMerge = React.createClass({ 204 | render: function() { 205 | var conflict = this.props.conflict; 206 | return
    207 |

    Merge Manually

    208 | 209 | 210 | 211 |
    212 | }, 213 | changeCompleted: function(ev) { 214 | this.props.conflict.completed = ev.target.checked; 215 | this.forceUpdate(); 216 | }, 217 | changeTitle: function(ev) { 218 | this.props.conflict.title = ev.target.value; 219 | this.forceUpdate(); 220 | } 221 | }); 222 | -------------------------------------------------------------------------------- /client/reapp/views/user.jsx: -------------------------------------------------------------------------------- 1 | (typeof exports !== 'undefined' ? exports : window).UserView = React.createClass({ 2 | render: function() { 3 | return
    4 | 5 |

    User

    6 | 7 |

    Under construction.

    8 | 9 |
    10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /client/reapp/views/welcome.jsx: -------------------------------------------------------------------------------- 1 | var Link = ReactRouter.Link; 2 | 3 | (typeof exports !== 'undefined' ? exports : window).WelcomeView = React.createClass({ 4 | render: function() { 5 | return
    6 | 7 |

    Welcome

    8 | 9 | This is the welcome view. View your todo list. 10 | 11 |
    12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /common/models/test/todo.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dundalek/loopback-example-full-stack-react/f5d63172ee1e3e19553dc1a879b9a541c6f17730/common/models/test/todo.test.js -------------------------------------------------------------------------------- /common/models/test/user.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dundalek/loopback-example-full-stack-react/f5d63172ee1e3e19553dc1a879b9a541c6f17730/common/models/test/user.test.js -------------------------------------------------------------------------------- /common/models/todo.js: -------------------------------------------------------------------------------- 1 | var loopback = require('loopback'); 2 | var async = require('async'); 3 | 4 | module.exports = function(Todo) { 5 | 6 | Todo.definition.properties.created.default = Date.now; 7 | 8 | Todo.beforeSave = function(next, model) { 9 | if (!model.id) model.id = 't-' + Math.floor(Math.random() * 10000).toString(); 10 | next(); 11 | }; 12 | 13 | Todo.stats = function(filter, cb) { 14 | var stats = {}; 15 | cb = arguments[arguments.length - 1]; 16 | var Todo = this; 17 | 18 | async.parallel([ 19 | countComplete, 20 | count 21 | ], function(err) { 22 | if (err) return cb(err); 23 | stats.remaining = stats.total - stats.completed; 24 | cb(null, stats); 25 | }); 26 | 27 | function countComplete(cb) { 28 | Todo.count({completed: true}, function(err, count) { 29 | stats.completed = count; 30 | cb(err); 31 | }); 32 | } 33 | 34 | function count(cb) { 35 | Todo.count(function(err, count) { 36 | stats.total = count; 37 | cb(err); 38 | }); 39 | } 40 | }; 41 | 42 | Todo.remoteMethod('stats', { 43 | accepts: {arg: 'filter', type: 'object'}, 44 | returns: {arg: 'stats', type: 'object'}, 45 | http: { path: '/stats' } 46 | }, Todo.stats); 47 | }; 48 | -------------------------------------------------------------------------------- /common/models/todo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo", 3 | "base": "PersistedModel", 4 | "trackChanges": true, 5 | "properties": { 6 | "id": { 7 | "id": true, 8 | "type": "string" 9 | }, 10 | "title": "string", 11 | "completed": { 12 | "type": "boolean", 13 | "default": false 14 | }, 15 | "created": { 16 | "type": "number" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /common/models/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "user", 3 | "base": "PersistedModel", 4 | "trackChanges": true, 5 | "properties": { 6 | "id": { 7 | "id": true, 8 | "type": "string" 9 | }, 10 | "title": "string", 11 | "completed": { 12 | "type": "boolean", 13 | "default": false 14 | }, 15 | "created": { 16 | "type": "number" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /global-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Global configuration shared by components. 3 | */ 4 | 5 | var pkg = require('./package.json'); 6 | 7 | // The path where to mount the REST API app 8 | exports.restApiRoot = '/api'; 9 | 10 | // The URL where the browser client can access the REST API is available 11 | // Replace with a full url (including hostname) if your client is being 12 | // served from a different server than your REST API. 13 | exports.restApiUrl = exports.restApiRoot; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-example-full-stack", 3 | "version": "0.0.1", 4 | "description": "LoopBack browser and server example", 5 | "main": "server/server.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "grunt test", 11 | "build": "bower install; grunt build" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/strongloop/loopback-example-full-stack.git" 16 | }, 17 | "keywords": [ 18 | "loopback", 19 | "example", 20 | "browser", 21 | "server" 22 | ], 23 | "author": "Ritchie Martori", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/strongloop/loopback-example-full-stack/issues" 27 | }, 28 | "homepage": "https://github.com/strongloop/loopback-example-full-stack", 29 | "devDependencies": { 30 | "bower": "^1.3.8", 31 | "browserify": "~4.2.3", 32 | "connect-livereload": "^0.4.0", 33 | "grunt": "^0.4.5", 34 | "grunt-autoprefixer": "^0.8.2", 35 | "grunt-concurrent": "^0.5.0", 36 | "grunt-contrib-clean": "^0.5.0", 37 | "grunt-contrib-concat": "^0.5.0", 38 | "grunt-contrib-connect": "^0.8.0", 39 | "grunt-contrib-copy": "^0.5.0", 40 | "grunt-contrib-cssmin": "^0.10.0", 41 | "grunt-contrib-htmlmin": "^0.3.0", 42 | "grunt-contrib-imagemin": "^0.8.1", 43 | "grunt-contrib-jshint": "^0.10.0", 44 | "grunt-contrib-uglify": "^0.5.1", 45 | "grunt-contrib-watch": "^0.6.1", 46 | "grunt-filerev": "^0.2.1", 47 | "grunt-google-cdn": "^0.4.0", 48 | "grunt-karma": "^0.8.3", 49 | "grunt-newer": "^0.7.0", 50 | "grunt-ng-annotate": "^0.8.0", 51 | "grunt-react": "^0.10.0", 52 | "grunt-svgmin": "^0.4.0", 53 | "grunt-usemin": "^2.3.0", 54 | "grunt-wiredep": "^1.8.0", 55 | "jshint-stylish": "^0.4.0", 56 | "karma": "^0.12.17", 57 | "karma-chrome-launcher": "^0.1.4", 58 | "karma-jasmine": "^0.1.5", 59 | "karma-phantomjs-launcher": "^0.1.4", 60 | "load-grunt-tasks": "^0.6.0", 61 | "time-grunt": "^0.4.0" 62 | }, 63 | "dependencies": { 64 | "async": "~0.9.0", 65 | "compression": "^1.0.9", 66 | "errorhandler": "^1.1.1", 67 | "loopback": "^2.0.0", 68 | "loopback-boot": "^2.0.0", 69 | "loopback-connector-mongodb": "^1.4.1", 70 | "loopback-datasource-juggler": "^2.0.0", 71 | "loopback-explorer": "^1.2.4" 72 | }, 73 | "engines": { 74 | "node": ">=0.10.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /server/boot/angular-routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | var routes = require('../../client/reapp/config/routes'); 3 | Object 4 | .keys(routes) 5 | .forEach(function(route) { 6 | app.get(route, function(req, res) { 7 | res.sendfile(app.get('indexFile')); 8 | }); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /server/boot/authentication.js: -------------------------------------------------------------------------------- 1 | module.exports = function(server) { 2 | 3 | // enable authentication 4 | // server.enableAuth(); 5 | }; 6 | -------------------------------------------------------------------------------- /server/boot/change-tracking.js: -------------------------------------------------------------------------------- 1 | module.exports = function(server) { 2 | 3 | // TODO(ritch) this should be unecessary soon.... 4 | var Todo = server.models.Todo; 5 | server.model(Todo.getChangeModel()); 6 | }; 7 | -------------------------------------------------------------------------------- /server/boot/dev-assets.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = function(app) { 4 | if (!app.get('isDevEnv')) return; 5 | 6 | var serveDir = app.loopback.static; 7 | 8 | app.use(serveDir(projectPath('.tmp'))); 9 | app.use('/bower_components', serveDir(projectPath('bower_components'))); 10 | app.use('/lbclient', serveDir(projectPath('client/lbclient'))); 11 | }; 12 | 13 | function projectPath(relative) { 14 | return path.resolve(__dirname, '../..', relative); 15 | } 16 | -------------------------------------------------------------------------------- /server/boot/explorer.js: -------------------------------------------------------------------------------- 1 | var explorer = require('loopback-explorer'); 2 | 3 | module.exports = function(server) { 4 | var restApiRoot = server.get('restApiRoot'); 5 | 6 | var explorerApp = explorer(server, { basePath: restApiRoot }); 7 | server.use('/explorer', explorerApp); 8 | server.once('started', function() { 9 | var baseUrl = server.get('url').replace(/\/$/, ''); 10 | console.log('Browse your REST API at %s%s', baseUrl, explorerApp.mountpath); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /server/boot/rest-api.js: -------------------------------------------------------------------------------- 1 | module.exports = function mountRestApi(server) { 2 | var restApiRoot = server.get('restApiRoot'); 3 | server.use(restApiRoot, server.loopback.rest()); 4 | }; 5 | -------------------------------------------------------------------------------- /server/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "localhost" 3 | } 4 | -------------------------------------------------------------------------------- /server/config.local.js: -------------------------------------------------------------------------------- 1 | var GLOBAL_CONFIG = require('../global-config'); 2 | 3 | var isDevEnv = (process.env.NODE_ENV || 'development') === 'development'; 4 | 5 | module.exports = { 6 | restApiRoot: GLOBAL_CONFIG.restApiRoot, 7 | livereload: process.env.LIVE_RELOAD, 8 | isDevEnv: isDevEnv, 9 | indexFile: require.resolve(isDevEnv ? 10 | '../client/reapp/index.html' : '../client/dist/index.html'), 11 | }; 12 | -------------------------------------------------------------------------------- /server/datasources.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "connector": "memory", 4 | "defaultForType": "db" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/datasources.production.js: -------------------------------------------------------------------------------- 1 | // Use the same environment-based configuration as in staging 2 | module.exports = require('./datasources.staging.js'); 3 | -------------------------------------------------------------------------------- /server/datasources.staging.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | db: { 3 | connector: 'mongodb', 4 | hostname: process.env.DB_HOST || 'localhost', 5 | port: process.env.DB_PORT || 27017, 6 | user: process.env.DB_USER, 7 | password: process.env.DB_PASSWORD, 8 | database: 'todo-example', 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /server/model-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "sources": ["../common/models"] 4 | }, 5 | "Todo": { 6 | "dataSource": "db" 7 | }, 8 | "user": { 9 | "dataSource": "db" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "main": "server.js" 4 | } 5 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var loopback = require('loopback'); 3 | var boot = require('loopback-boot'); 4 | 5 | var app = module.exports = loopback(); 6 | 7 | // middleware 8 | app.use(loopback.compress()); 9 | 10 | // it's important to register the livereload middleware 11 | // after any response-processing middleware like compress, 12 | // but before any middleware serving actual content 13 | var livereload = app.get('livereload'); 14 | if (livereload) { 15 | app.use(require('connect-livereload')({ 16 | port: livereload 17 | })); 18 | } 19 | 20 | // boot scripts mount components like REST API 21 | boot(app, __dirname); 22 | 23 | // Mount static files like ngapp 24 | // All static middleware should be registered at the end, as all requests 25 | // passing the static middleware are hitting the file system 26 | app.use(loopback.static(path.dirname(app.get('indexFile')))); 27 | 28 | // Requests that get this far won't be handled 29 | // by any middleware. Convert them into a 404 error 30 | // that will be handled later down the chain. 31 | app.use(loopback.urlNotFound()); 32 | 33 | // The ultimate error handler. 34 | app.use(loopback.errorHandler()); 35 | 36 | // optionally start the app 37 | app.start = function() { 38 | // start the web server 39 | return app.listen(function() { 40 | app.emit('started'); 41 | console.log('Web server listening at: %s', app.get('url')); 42 | }); 43 | }; 44 | 45 | if (require.main === module) { 46 | app.start(); 47 | } 48 | --------------------------------------------------------------------------------