diff --git a/app/components/img-fancy-load.js b/app/components/img-fancy-load.js index dc7e9e5..62c7460 100644 --- a/app/components/img-fancy-load.js +++ b/app/components/img-fancy-load.js @@ -3,7 +3,7 @@ import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object' export default class SearchBooksComponent extends Component { - placeholderSrc = 'http://placehold.jp/d1d1d1/707070/400x300.png?text=No%20Image'; + placeholderSrc = 'placeholder-img.png'; @tracked showPlaceholder = true; @action @@ -12,8 +12,8 @@ export default class SearchBooksComponent extends Component { } @action - error() { - this.src = this.placeholderSrc; + error(event) { + event.target.src = this.placeholderSrc; this.showPlaceholder = false; } } diff --git a/app/components/list-books.hbs b/app/components/list-books.hbs index a8c6ca7..9a884f5 100644 --- a/app/components/list-books.hbs +++ b/app/components/list-books.hbs @@ -4,4 +4,10 @@ -{{/each}} \ No newline at end of file +{{/each}} + +{{#if @model }} + + + +{{/if}} \ No newline at end of file diff --git a/app/models/book.js b/app/models/book.js index 16f69a8..6d3e619 100644 --- a/app/models/book.js +++ b/app/models/book.js @@ -9,6 +9,6 @@ export default class BookModel extends Model { get image() { const firstImage = this.photos.get('firstObject'); - return firstImage ? firstImage : { uri: 'http://placehold.jp/d1d1d1/707070/400x300.png?text=No%20Image', title: 'No Image' }; + return firstImage ? firstImage : { uri: 'placeholder-img.png', title: 'No Image' }; } } diff --git a/app/routes/books.js b/app/routes/books.js index bf06fc8..29e9232 100644 --- a/app/routes/books.js +++ b/app/routes/books.js @@ -4,13 +4,16 @@ import ENV from 'bigriver-bookstore/config/environment' export default class BooksRoute extends Route { @service store; + @service infinity; model() { - return this.store.query('book', { - page: { - size: ENV.BOOKS_PER_PAGE - }, - include: 'author,photos' + return this.infinity.model('book', { + include: 'author,photos', + perPage: ENV.BOOKS_PER_PAGE, + startingPage: 1, + perPageParam: 'page[size]', + pageParam: 'page[number]', + countParam: 'meta.total_resources' }); } } diff --git a/app/routes/search.js b/app/routes/search.js index e6a4b80..3a95201 100644 --- a/app/routes/search.js +++ b/app/routes/search.js @@ -4,6 +4,7 @@ import ENV from 'bigriver-bookstore/config/environment' export default class SearchRoute extends Route { @service store; + @service infinity; queryParams = { query: { @@ -14,14 +15,16 @@ export default class SearchRoute extends Route { model(params) { const query = params.query ? params.query : ''; - return this.store.query('book', { - page: { - size: ENV.BOOKS_PER_PAGE, - }, + return this.infinity.model('book', { filter: { 'author.name': query }, - include: 'author,photos' + include: 'author,photos', + perPage: ENV.BOOKS_PER_PAGE, + startingPage: 1, + perPageParam: 'page[size]', + pageParam: 'page[number]', + countParam: 'meta.total_resources' }); } } diff --git a/app/styles/app.css b/app/styles/app.css index 02dfca7..d2a0c6f 100644 --- a/app/styles/app.css +++ b/app/styles/app.css @@ -27,7 +27,7 @@ h1 { bottom: 0; left: 0; background-color: #ccc; - background-image: url('http://placehold.it/400x300?text=Loading Image'); + background-image: url('/loading-img.png'); background-repeat: no-repeat; background-size: 100%; } @@ -39,4 +39,17 @@ h1 { footer .badge { font-size: 0.9rem; +} + +.infinity-loader { + margin-right: auto; + margin-left: auto; +} + +.infinity-loader img { + width: 100px; +} + +.infinity-loader.reached-infinity img { + display: none; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 28c8ca3..192d90e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1486,6 +1486,16 @@ } } }, + "@ember/render-modifiers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@ember/render-modifiers/-/render-modifiers-1.0.2.tgz", + "integrity": "sha512-6tEnHl5+62NTSAG2mwhGMFPhUrJQjoVqV+slsn+rlTknm2Zik+iwxBQEbwaiQOU1FUYxkS8RWcieovRNMR8inQ==", + "dev": true, + "requires": { + "ember-cli-babel": "^7.10.0", + "ember-modifier-manager-polyfill": "^1.1.0" + } + }, "@ember/test-helpers": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@ember/test-helpers/-/test-helpers-1.7.1.tgz", @@ -8382,6 +8392,93 @@ "focus-trap": "^5.0.1" } }, + "ember-in-viewport": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/ember-in-viewport/-/ember-in-viewport-3.7.2.tgz", + "integrity": "sha512-Jv664t0cPiAMv6SOuQS+JmMfFeLnhGlhK06y5Hytolauwp8XJWDoq44wqQ7+oyCJEhf5bL+sam5tENep6euA9Q==", + "dev": true, + "requires": { + "ember-auto-import": "^1.5.2", + "ember-cli-babel": "^7.7.3", + "ember-modifier": "^1.0.2", + "fast-deep-equal": "^2.0.1", + "intersection-observer-admin": "~0.2.12", + "raf-pool": "~0.1.4" + }, + "dependencies": { + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + } + } + }, + "ember-infinity": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ember-infinity/-/ember-infinity-2.1.1.tgz", + "integrity": "sha512-tJrSYqcZCT0quLeu+IoXy46TAMaFtTRPF0jgHgKfa9xP4NvHDqENwdLpMewlkS4vbs3SCkxo4yBtQw5JG9GuEw==", + "dev": true, + "requires": { + "@ember/render-modifiers": "^1.0.2", + "ember-cli-babel": "~7.11.0", + "ember-cli-htmlbars": "^3.0.1", + "ember-in-viewport": "~3.7.2" + }, + "dependencies": { + "ember-cli-babel": { + "version": "7.11.1", + "resolved": "https://registry.npmjs.org/ember-cli-babel/-/ember-cli-babel-7.11.1.tgz", + "integrity": "sha512-Qgd7y9NVbRLEtwjBW/vPHXdTQrIgfgoCSFHfvBpEmLuWSWNpE/J6qwXrSbB9nEIlfzyjH0Almv4m0jwuJsB3ow==", + "dev": true, + "requires": { + "@babel/core": "^7.0.0", + "@babel/plugin-proposal-class-properties": "^7.3.4", + "@babel/plugin-proposal-decorators": "^7.3.0", + "@babel/plugin-transform-modules-amd": "^7.0.0", + "@babel/plugin-transform-runtime": "^7.2.0", + "@babel/polyfill": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "@babel/runtime": "^7.2.0", + "amd-name-resolver": "^1.2.1", + "babel-plugin-debug-macros": "^0.3.0", + "babel-plugin-ember-modules-api-polyfill": "^2.12.0", + "babel-plugin-module-resolver": "^3.1.1", + "broccoli-babel-transpiler": "^7.3.0", + "broccoli-debug": "^0.6.4", + "broccoli-funnel": "^2.0.1", + "broccoli-source": "^1.1.0", + "clone": "^2.1.2", + "ember-cli-babel-plugin-helpers": "^1.1.0", + "ember-cli-version-checker": "^2.1.2", + "ensure-posix-path": "^1.0.2", + "semver": "^5.5.0" + } + }, + "ember-cli-htmlbars": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ember-cli-htmlbars/-/ember-cli-htmlbars-3.1.0.tgz", + "integrity": "sha512-cgvRJM73IT0aePUG7oQ/afB7vSRBV3N0wu9BrWhHX2zkR7A7cUBI7KC9VPk6tbctCXoM7BRGsCC4aIjF7yrfXA==", + "dev": true, + "requires": { + "broccoli-persistent-filter": "^2.3.1", + "hash-for-dep": "^1.5.1", + "json-stable-stringify": "^1.0.1", + "strip-bom": "^3.0.0" + } + }, + "ember-cli-version-checker": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ember-cli-version-checker/-/ember-cli-version-checker-2.2.0.tgz", + "integrity": "sha512-G+KtYIVlSOWGcNaTFHk76xR4GdzDLzAS4uxZUKdASuFX0KJE43C6DaqL+y3VTpUFLI2FIkAS6HZ4I1YBi+S3hg==", + "dev": true, + "requires": { + "resolve": "^1.3.3", + "semver": "^5.3.0" + } + } + } + }, "ember-inflector": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ember-inflector/-/ember-inflector-3.0.1.tgz", @@ -9099,6 +9196,19 @@ "ember-cli-babel": "^7.1.0" } }, + "ember-modifier": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ember-modifier/-/ember-modifier-1.0.3.tgz", + "integrity": "sha512-vWuFyvdkULUyasvEXxe5lcfuPZV/Uqe+b0IQ1yU+TY1RSJnFdVUu/CVHT8Bu4HUJInqzAihwPMTwty7fypzi5Q==", + "dev": true, + "requires": { + "ember-cli-babel": "^7.11.1", + "ember-cli-is-package-missing": "^1.0.0", + "ember-cli-normalize-entity-name": "^1.0.0", + "ember-cli-string-utils": "^1.1.0", + "ember-modifier-manager-polyfill": "^1.2.0" + } + }, "ember-modifier-manager-polyfill": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/ember-modifier-manager-polyfill/-/ember-modifier-manager-polyfill-1.2.0.tgz", @@ -13138,6 +13248,12 @@ "through": "^2.3.6" } }, + "intersection-observer-admin": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/intersection-observer-admin/-/intersection-observer-admin-0.2.12.tgz", + "integrity": "sha512-97+A29MV0kp6Xzkb4CxBNxxDU4qldyVFNzvZIiLMYqIZFutT2DJCzE1TEv0hXdmFQfAIY4KhNhL6L1BEEU0J8w==", + "dev": true + }, "into-stream": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", @@ -15693,6 +15809,12 @@ "broccoli-merge-trees": "^3.0.1" } }, + "raf-pool": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/raf-pool/-/raf-pool-0.1.4.tgz", + "integrity": "sha512-BBPamTVuSprPq7CUmgxc+ycbsYUtUYnQtJYEfMHXMaostPaNpQzipLfSa/rwjmlgjBPiD7G+I+8W340sLOPu6g==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", diff --git a/package.json b/package.json index 9b11e62..81c0b26 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "ember-cli-uglify": "^3.0.0", "ember-data": "~3.14.0", "ember-export-application-global": "^2.0.0", + "ember-infinity": "^2.1.1", "ember-load-initializers": "^2.0.0", "ember-maybe-import-regenerator": "^0.1.6", "ember-qunit": "^4.4.1", diff --git a/public/loading-img.png b/public/loading-img.png new file mode 100644 index 0000000..3510d3d Binary files /dev/null and b/public/loading-img.png differ diff --git a/public/placeholder-img.png b/public/placeholder-img.png new file mode 100644 index 0000000..31d6b64 Binary files /dev/null and b/public/placeholder-img.png differ diff --git a/public/spinner.gif b/public/spinner.gif new file mode 100644 index 0000000..cfec910 Binary files /dev/null and b/public/spinner.gif differ diff --git a/tests/acceptance/books-test.js b/tests/acceptance/books-test.js index 96912d1..e0e553b 100644 --- a/tests/acceptance/books-test.js +++ b/tests/acceptance/books-test.js @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { visit, currentURL } from '@ember/test-helpers'; +import { visit, currentURL, waitUntil } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import page from 'bigriver-bookstore/tests/pages/books'; @@ -25,4 +25,18 @@ module('Acceptance | books', function(hooks) { assert.ok(page.books(0).hasAuthor); assert.ok(page.books(0).hasImage); }); + + test('fetch more data when scrolled down', async function(assert) { + await page.visit(); + + assert.equal(page.books().count, 9); + + document.querySelector(page.loaderClass).scrollIntoView(); + + await waitUntil(() => { + return page.books().count === 18; + }); + + assert.equal(page.books().count, 18); + }); }); diff --git a/tests/pages/books.js b/tests/pages/books.js index 9a1ec9d..7597c1a 100644 --- a/tests/pages/books.js +++ b/tests/pages/books.js @@ -19,4 +19,5 @@ export default create({ searchDisabledPresent: isPresent('[data-test="search-btn"]:disabled'), fillInSearchInput: fillable('input', { scope: '[data-test="search-by-author-form"]' }), fancyPlaceholder: isPresent('[data-test="fancy-placeholder"]'), + loaderClass: '.infinity-loader', }); diff --git a/tests/unit/models/book-test.js b/tests/unit/models/book-test.js index 576b224..32005ba 100644 --- a/tests/unit/models/book-test.js +++ b/tests/unit/models/book-test.js @@ -23,9 +23,9 @@ module('Unit | Model | book', function(hooks) { test('when no image provided return the placeholder', function (assert) { const store = this.owner.lookup('service:store'); - const placeholder = { uri: 'http://placehold.jp/d1d1d1/707070/400x300.png?text=No%20Image', title: 'No Image' }; + const placeholder = { uri: 'placeholder-img.png', title: 'No Image' }; const model = store.createRecord('book', { title: 'Book #1' }); - assert.equal(model.image.uri, 'http://placehold.jp/d1d1d1/707070/400x300.png?text=No%20Image'); + assert.equal(model.image.uri, 'placeholder-img.png'); }); });