diff --git a/app/adapters/application.js b/app/adapters/application.js
new file mode 100644
index 0000000..f1b1ab1
--- /dev/null
+++ b/app/adapters/application.js
@@ -0,0 +1,5 @@
+import DS from 'ember-data';
+
+export default DS.JSONAPIAdapter.extend({
+ namespace: 'api'
+});
diff --git a/app/components/item-listing.js b/app/components/item-listing.js
new file mode 100644
index 0000000..926b613
--- /dev/null
+++ b/app/components/item-listing.js
@@ -0,0 +1,4 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+});
diff --git a/app/components/list-filter.js b/app/components/list-filter.js
new file mode 100644
index 0000000..e5b72da
--- /dev/null
+++ b/app/components/list-filter.js
@@ -0,0 +1,20 @@
+import Ember from 'ember';
+
+export default Ember.Component.extend({
+ classNames: ['list-filter'],
+ value: '',
+
+ init() {
+ this._super(...arguments);
+ this.get('filter')('').then((results) => this.set('results', results));
+ },
+
+ actions: {
+ handleFilterEntry() {
+ let filterInputValue = this.get('value');
+ let filterAction = this.get('filter');
+ filterAction(filterInputValue).then((filterResults) => this.set('results', filterResults));
+ }
+ }
+
+});
diff --git a/app/controllers/items.js b/app/controllers/items.js
new file mode 100644
index 0000000..004e97d
--- /dev/null
+++ b/app/controllers/items.js
@@ -0,0 +1,13 @@
+import Ember from 'ember';
+
+export default Ember.Controller.extend({
+ actions: {
+ filterByName(param) {
+ if (param !== '') {
+ return this.get('store').query('item', { name: param });
+ } else {
+ return this.get('store').findAll('item');
+ }
+ }
+ }
+});
diff --git a/app/controllers/items/index.js b/app/controllers/items/index.js
new file mode 100644
index 0000000..c3cd6ec
--- /dev/null
+++ b/app/controllers/items/index.js
@@ -0,0 +1,3 @@
+import ItemsController from '../items';
+
+export default ItemsController;
diff --git a/app/models/item.js b/app/models/item.js
new file mode 100644
index 0000000..ce75458
--- /dev/null
+++ b/app/models/item.js
@@ -0,0 +1,11 @@
+import DS from 'ember-data';
+
+export default DS.Model.extend({
+ title: DS.attr(),
+ owner: DS.attr(),
+ location: DS.attr(),
+ type: DS.attr(),
+ image: DS.attr(),
+ bedrooms: DS.attr(),
+ description: DS.attr()
+});
diff --git a/app/router.js b/app/router.js
index cdc2578..698f36b 100644
--- a/app/router.js
+++ b/app/router.js
@@ -7,6 +7,10 @@ const Router = Ember.Router.extend({
});
Router.map(function() {
+ this.route('about');
+ this.route('items', function() {
+ this.route('show', {path: '/:item_id'});
+ });
});
export default Router;
diff --git a/app/routes/about.js b/app/routes/about.js
new file mode 100644
index 0000000..26d9f31
--- /dev/null
+++ b/app/routes/about.js
@@ -0,0 +1,4 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+});
diff --git a/app/routes/index.js b/app/routes/index.js
new file mode 100644
index 0000000..66009cf
--- /dev/null
+++ b/app/routes/index.js
@@ -0,0 +1,8 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ beforeModel() {
+ this._super(...arguments);
+ this.replaceWith('items');
+ }
+});
diff --git a/app/routes/items.js b/app/routes/items.js
new file mode 100644
index 0000000..26d9f31
--- /dev/null
+++ b/app/routes/items.js
@@ -0,0 +1,4 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+});
diff --git a/app/routes/items/index.js b/app/routes/items/index.js
new file mode 100644
index 0000000..dd06f61
--- /dev/null
+++ b/app/routes/items/index.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model() {
+ return this.get('store').findAll('item');
+ }
+});
diff --git a/app/routes/items/show.js b/app/routes/items/show.js
new file mode 100644
index 0000000..fc24043
--- /dev/null
+++ b/app/routes/items/show.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model(params) {
+ return this.get('store').findRecord('item', params.item_id);
+ }
+});
diff --git a/app/templates/about.hbs b/app/templates/about.hbs
new file mode 100644
index 0000000..0e051a0
--- /dev/null
+++ b/app/templates/about.hbs
@@ -0,0 +1,9 @@
+
+
+
About Super Rentals
+
+ The Super Rentals website is a delightful project created to explore Ember.
+ By building a property rental site, we can simultaneously imagine traveling
+ AND building Ember applications.
+
+
diff --git a/app/templates/application.hbs b/app/templates/application.hbs
new file mode 100644
index 0000000..e599a76
--- /dev/null
+++ b/app/templates/application.hbs
@@ -0,0 +1,17 @@
+
diff --git a/app/templates/components/item-listing.hbs b/app/templates/components/item-listing.hbs
new file mode 100644
index 0000000..9b564c4
--- /dev/null
+++ b/app/templates/components/item-listing.hbs
@@ -0,0 +1,15 @@
+
+ {{#link-to "items.show" item}}{{item.title}}{{/link-to}}
+
+ Owner: {{item.owner}}
+
+
+ Type: {{item.type}}
+
+
+ Location: {{item.location}}
+
+
+ Number of bedrooms: {{item.bedrooms}}
+
+
diff --git a/app/templates/components/list-filter.hbs b/app/templates/components/list-filter.hbs
new file mode 100644
index 0000000..e91f11a
--- /dev/null
+++ b/app/templates/components/list-filter.hbs
@@ -0,0 +1,2 @@
+{{input value=value key-up=(action 'handleFilterEntry') class="light" placeholder="Filter By Name"}}
+{{yield results}}
diff --git a/app/templates/index.hbs b/app/templates/index.hbs
new file mode 100644
index 0000000..c24cd68
--- /dev/null
+++ b/app/templates/index.hbs
@@ -0,0 +1 @@
+{{outlet}}
diff --git a/app/templates/items.hbs b/app/templates/items.hbs
new file mode 100644
index 0000000..c24cd68
--- /dev/null
+++ b/app/templates/items.hbs
@@ -0,0 +1 @@
+{{outlet}}
diff --git a/app/templates/items/index.hbs b/app/templates/items/index.hbs
new file mode 100644
index 0000000..e1b0f64
--- /dev/null
+++ b/app/templates/items/index.hbs
@@ -0,0 +1,10 @@
+{{#list-filter
+ filter=(action 'filterByName') as |items| }}
+
+
+ {{#each items as |itemUnit|}}
+ - {{item-listing item=itemUnit}}
+ {{/each}}
+
+{{/list-filter}}
+{{outlet}}
diff --git a/app/templates/items/show.hbs b/app/templates/items/show.hbs
new file mode 100644
index 0000000..1ee7708
--- /dev/null
+++ b/app/templates/items/show.hbs
@@ -0,0 +1,22 @@
+
+
{{model.title}}
+
+
+ Owner: {{model.owner}}
+
+
+ Type: {{model.type}}
+
+
+ Location: {{model.city}}
+
+
+ Number of bedrooms: {{model.bedrooms}}
+
+
{{model.description}}
+
+

+
+
+
+{{outlet}}
diff --git a/bower.json b/bower.json
index 1177fac..573cbde 100644
--- a/bower.json
+++ b/bower.json
@@ -2,6 +2,7 @@
"name": "ember-quickstart",
"dependencies": {
"ember": "~2.9.0",
- "ember-cli-shims": "0.1.3"
+ "ember-cli-shims": "0.1.3",
+ "bootstrap": "~3.3.5"
}
}
diff --git a/ember-cli-build.js b/ember-cli-build.js
index 2537ce2..0336b37 100644
--- a/ember-cli-build.js
+++ b/ember-cli-build.js
@@ -5,6 +5,9 @@ var EmberApp = require('ember-cli/lib/broccoli/ember-app');
module.exports = function(defaults) {
var app = new EmberApp(defaults, {
// Add options here
+ 'ember-bootstrap': {
+ 'importBootstrapTheme': true
+ }
});
// Use `app.import` to add additional libraries to the generated
diff --git a/mirage/config.js b/mirage/config.js
new file mode 100644
index 0000000..231a2f9
--- /dev/null
+++ b/mirage/config.js
@@ -0,0 +1,56 @@
+export default function() {
+ this.namespace = '/api';
+
+ let items = [{
+ type: 'items',
+ id: 'grand-old-mansion',
+ attributes: {
+ title: 'Grand Old Mansion',
+ owner: 'Veruca Salt',
+ city: 'San Francisco',
+ type: 'Estate',
+ bedrooms: 15,
+ image: 'https://upload.wikimedia.org/wikipedia/commons/c/cb/Crane_estate_(5).jpg',
+ description: "This grand old mansion sits on over 100 acres of rolling hills and dense redwood forests."
+ }
+ }, {
+ type: 'items',
+ id: 'urban-living',
+ attributes: {
+ title: 'Urban Living',
+ owner: 'Mike Teavee',
+ city: 'Seattle',
+ type: 'Condo',
+ bedrooms: 1,
+ image: 'https://upload.wikimedia.org/wikipedia/commons/0/0e/Alfonso_13_Highrise_Tegucigalpa.jpg',
+ description: "A commuters dream. This rental is within walking distance of 2 bus stops and the Metro."
+ }
+ }, {
+ type: 'items',
+ id: 'downtown-charm',
+ attributes: {
+ title: 'Downtown Charm',
+ owner: 'Violet Beauregarde',
+ city: 'Portland',
+ type: 'Apartment',
+ bedrooms: 3,
+ image: 'https://upload.wikimedia.org/wikipedia/commons/f/f7/Wheeldon_Apartment_Building_-_Portland_Oregon.jpg',
+ description: "Convenience is at your doorstep with this charming downtown rental. Great restaurants and active night life are within a few feet."
+ }
+ }];
+
+ this.get('/items', function(db, request) {
+ if(request.queryParams.name !== undefined) {
+ let filteredItems = items.filter(function(i) {
+ return i.attributes.title.toLowerCase().indexOf(request.queryParams.name.toLowerCase()) !== -1;
+ });
+ return { data: filteredItems };
+ } else {
+ return { data: items };
+ }
+ });
+
+ this.get('/items/:id', function (db, request) {
+ return { data: items.find((item) => request.params.id === item.id) };
+ });
+}
diff --git a/mirage/scenarios/default.js b/mirage/scenarios/default.js
new file mode 100644
index 0000000..0d2db8d
--- /dev/null
+++ b/mirage/scenarios/default.js
@@ -0,0 +1,11 @@
+export default function(/* server */) {
+
+ /*
+ Seed your development database using your factories.
+ This data will not be loaded in your tests.
+
+ Make sure to define a factory for each model you want to create.
+ */
+
+ // server.createList('post', 10);
+}
diff --git a/mirage/serializers/application.js b/mirage/serializers/application.js
new file mode 100644
index 0000000..6d47a36
--- /dev/null
+++ b/mirage/serializers/application.js
@@ -0,0 +1,4 @@
+import { JSONAPISerializer } from 'ember-cli-mirage';
+
+export default JSONAPISerializer.extend({
+});
diff --git a/package.json b/package.json
index f1782de..edd63a1 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"devDependencies": {
"broccoli-asset-rev": "^2.4.5",
"ember-ajax": "^2.4.1",
+ "ember-bootstrap": "0.11.2",
"ember-cli": "2.9.1",
"ember-cli-app-version": "^2.0.0",
"ember-cli-babel": "^5.1.7",
@@ -29,6 +30,7 @@
"ember-cli-htmlbars-inline-precompile": "^0.3.3",
"ember-cli-inject-live-reload": "^1.4.1",
"ember-cli-jshint": "^1.0.4",
+ "ember-cli-mirage": "0.2.4",
"ember-cli-qunit": "^3.0.1",
"ember-cli-release": "^0.2.9",
"ember-cli-sri": "^2.1.0",
diff --git a/tests/acceptance/list-items-test.js b/tests/acceptance/list-items-test.js
new file mode 100644
index 0000000..6022c95
--- /dev/null
+++ b/tests/acceptance/list-items-test.js
@@ -0,0 +1,47 @@
+import { test } from 'qunit';
+import moduleForAcceptance from 'ember-quickstart/tests/helpers/module-for-acceptance';
+
+moduleForAcceptance('Acceptance | list items');
+
+test('visiting /items', function(assert) {
+ visit('/items');
+ andThen(function() {
+ assert.equal(currentURL(), '/items');
+ });
+});
+
+test('should link to information about the company.', function (assert) {
+ visit('/items');
+ click('a:contains("About")');
+ andThen(function () {
+ assert.equal(currentURL(), '/about', 'should navigate to about');
+ });
+});
+
+test('should list available rentals.', function (assert) {
+ visit('/items');
+
+ andThen(function() {
+ assert.equal(find('.listing').length, 3, 'should see 3 listings');
+ });
+});
+
+test('should filter the list of rentals by city.', function (assert) {
+ visit('/items');
+ fillIn('.list-filter input', 'urban');
+ keyEvent('.list-filter input', 'keyup', 69);
+ andThen(function () {
+ assert.equal(find('.listing').length, 1, 'should show 1 listing');
+ assert.equal(find('.listing .name:contains("Urban Living")').length, 1, 'should contain 1 listing with name Urban Living');
+ });
+});
+
+test('should show details for a specific rental', function (assert) {
+ visit('/items');
+ click('a:contains("Grand Old Mansion")');
+ andThen(function() {
+ assert.equal(currentURL(), '/items/grand-old-mansion', 'should navigate to show route');
+ assert.equal(find('.show-listing h2').text(), "Grand Old Mansion", 'should list rental title');
+ assert.equal(find('.description').length, 1, 'should list a description of the property');
+ });
+});
diff --git a/tests/integration/components/item-listing-test.js b/tests/integration/components/item-listing-test.js
new file mode 100644
index 0000000..e01649f
--- /dev/null
+++ b/tests/integration/components/item-listing-test.js
@@ -0,0 +1,21 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+
+moduleForComponent('item-listing', 'Integration | Component | item listing', {
+ integration: true
+});
+
+test('it renders', function(assert) {
+ this.render(hbs`{{item-listing}}`);
+
+ assert.equal(this.$().text().trim(), '');
+
+ // Template block usage:
+ this.render(hbs`
+ {{#item-listing}}
+ template block text
+ {{/item-listing}}
+ `);
+
+ assert.equal(this.$().text().trim(), 'template block text');
+});
diff --git a/tests/integration/components/list-filter-test.js b/tests/integration/components/list-filter-test.js
new file mode 100644
index 0000000..0330215
--- /dev/null
+++ b/tests/integration/components/list-filter-test.js
@@ -0,0 +1,73 @@
+import { moduleForComponent, test } from 'ember-qunit';
+import hbs from 'htmlbars-inline-precompile';
+import wait from 'ember-test-helpers/wait';
+import RSVP from 'rsvp';
+
+moduleForComponent('list-filter', 'Integration | Component | list filter', {
+ integration: true
+});
+
+const ITEMS = [{title: 'Grand Old Mansion'}, {title: 'Urban Living'}, {title: 'Downtown Charm'}];
+const FILTERED_ITEMS = [{title: 'Grand Old Mansion'}];
+
+test('should initially load all listings', function (assert) {
+ // we want our actions to return promises, since they are potentially fetching data asynchronously
+ this.on('filterByName', (val) => {
+ if (val === '') {
+ return RSVP.resolve(ITEMS);
+ } else {
+ return RSVP.resolve(FILTERED_ITEMS);
+ }
+ });
+
+ // with an integration test, you can set up and use your component in the same way your application
+ // will use it.
+ this.render(hbs`
+ {{#list-filter filter=(action 'filterByName') as |results|}}
+
+ {{#each results as |item|}}
+ -
+ {{item.title}}
+
+ {{/each}}
+
+ {{/list-filter}}
+ `);
+
+ // the wait function will return a promise that will wait for all promises
+ // and xhr requests to resolve before running the contents of the then block.
+ return wait().then(() => {
+ assert.equal(this.$('.name').length, 3);
+ assert.equal(this.$('.name').first().text().trim(), 'Grand Old Mansion');
+ });
+});
+
+test('should update with matching listings', function (assert) {
+ this.on('filterByName', (val) => {
+ if (val === '') {
+ return RSVP.resolve(ITEMS);
+ } else {
+ return RSVP.resolve(FILTERED_ITEMS);
+ }
+ });
+
+ this.render(hbs`
+ {{#list-filter filter=(action 'filterByName') as |results|}}
+
+ {{#each results as |item|}}
+ -
+ {{item.title}}
+
+ {{/each}}
+
+ {{/list-filter}}
+ `);
+
+ // The keyup event here should invoke an action that will cause the list to be filtered
+ this.$('.list-filter input').val('Grand').keyup();
+
+ return wait().then(() => {
+ assert.equal(this.$('.name').length, 1);
+ assert.equal(this.$('.name').text().trim(), 'Grand Old Mansion');
+ });
+});
diff --git a/tests/unit/adapters/application-test.js b/tests/unit/adapters/application-test.js
new file mode 100644
index 0000000..f0a2101
--- /dev/null
+++ b/tests/unit/adapters/application-test.js
@@ -0,0 +1,12 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('adapter:application', 'Unit | Adapter | application', {
+ // Specify the other units that are required for this test.
+ // needs: ['serializer:foo']
+});
+
+// Replace this with your real tests.
+test('it exists', function(assert) {
+ let adapter = this.subject();
+ assert.ok(adapter);
+});
diff --git a/tests/unit/controllers/items-test.js b/tests/unit/controllers/items-test.js
new file mode 100644
index 0000000..3a45297
--- /dev/null
+++ b/tests/unit/controllers/items-test.js
@@ -0,0 +1,12 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('controller:items', 'Unit | Controller | items', {
+ // Specify the other units that are required for this test.
+ // needs: ['controller:foo']
+});
+
+// Replace this with your real tests.
+test('it exists', function(assert) {
+ let controller = this.subject();
+ assert.ok(controller);
+});
diff --git a/tests/unit/controllers/items/index-test.js b/tests/unit/controllers/items/index-test.js
new file mode 100644
index 0000000..9a39902
--- /dev/null
+++ b/tests/unit/controllers/items/index-test.js
@@ -0,0 +1,12 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('controller:items/index', 'Unit | Controller | items/index', {
+ // Specify the other units that are required for this test.
+ // needs: ['controller:foo']
+});
+
+// Replace this with your real tests.
+test('it exists', function(assert) {
+ let controller = this.subject();
+ assert.ok(controller);
+});
diff --git a/tests/unit/models/item-test.js b/tests/unit/models/item-test.js
new file mode 100644
index 0000000..378a318
--- /dev/null
+++ b/tests/unit/models/item-test.js
@@ -0,0 +1,12 @@
+import { moduleForModel, test } from 'ember-qunit';
+
+moduleForModel('item', 'Unit | Model | item', {
+ // Specify the other units that are required for this test.
+ needs: []
+});
+
+test('it exists', function(assert) {
+ let model = this.subject();
+ // let store = this.store();
+ assert.ok(!!model);
+});
diff --git a/tests/unit/routes/about-test.js b/tests/unit/routes/about-test.js
new file mode 100644
index 0000000..e4e647c
--- /dev/null
+++ b/tests/unit/routes/about-test.js
@@ -0,0 +1,11 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('route:about', 'Unit | Route | about', {
+ // Specify the other units that are required for this test.
+ // needs: ['controller:foo']
+});
+
+test('it exists', function(assert) {
+ let route = this.subject();
+ assert.ok(route);
+});
diff --git a/tests/unit/routes/index-test.js b/tests/unit/routes/index-test.js
new file mode 100644
index 0000000..5d0f50d
--- /dev/null
+++ b/tests/unit/routes/index-test.js
@@ -0,0 +1,11 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('route:index', 'Unit | Route | index', {
+ // Specify the other units that are required for this test.
+ // needs: ['controller:foo']
+});
+
+test('it exists', function(assert) {
+ let route = this.subject();
+ assert.ok(route);
+});
diff --git a/tests/unit/routes/items-test.js b/tests/unit/routes/items-test.js
new file mode 100644
index 0000000..a175054
--- /dev/null
+++ b/tests/unit/routes/items-test.js
@@ -0,0 +1,11 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('route:items', 'Unit | Route | items', {
+ // Specify the other units that are required for this test.
+ // needs: ['controller:foo']
+});
+
+test('it exists', function(assert) {
+ let route = this.subject();
+ assert.ok(route);
+});
diff --git a/tests/unit/routes/items/index-test.js b/tests/unit/routes/items/index-test.js
new file mode 100644
index 0000000..bf147d5
--- /dev/null
+++ b/tests/unit/routes/items/index-test.js
@@ -0,0 +1,11 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('route:items/index', 'Unit | Route | items/index', {
+ // Specify the other units that are required for this test.
+ // needs: ['controller:foo']
+});
+
+test('it exists', function(assert) {
+ let route = this.subject();
+ assert.ok(route);
+});
diff --git a/tests/unit/routes/items/show-test.js b/tests/unit/routes/items/show-test.js
new file mode 100644
index 0000000..57dc736
--- /dev/null
+++ b/tests/unit/routes/items/show-test.js
@@ -0,0 +1,11 @@
+import { moduleFor, test } from 'ember-qunit';
+
+moduleFor('route:items/show', 'Unit | Route | items/show', {
+ // Specify the other units that are required for this test.
+ // needs: ['controller:foo']
+});
+
+test('it exists', function(assert) {
+ let route = this.subject();
+ assert.ok(route);
+});