diff --git a/app/components/thank-you-container.js b/app/components/thank-you-container.js index 192171417..ebba69121 100644 --- a/app/components/thank-you-container.js +++ b/app/components/thank-you-container.js @@ -2,11 +2,85 @@ import Ember from 'ember'; const { Component, + computed, + computed: { filter, mapBy }, + get, inject: { service } } = Ember; +const MAX_VOLUNTEERS = 12; + +/** + The `thank-you-container` component presents the main content for the + `thank-you` donations page. This includes an icon, thank you message and + a list of contributors. + + ## Default Usage + + ```handlebars + {{thank-you-container project=project}} + ``` + + @class thank-you-container + @module Component + @extends Ember.Component + */ export default Component.extend({ classNames: ['thank-you-container'], - onboarding: service() + onboarding: service(), + + /** + A filter for the project's approved users + + @property approvedProjectUsers + @type Ember.Array + */ + approvedProjectUsers: filter('project.projectUsers', function(projectUser) { + return get(projectUser, 'role') !== 'pending'; + }), + + /** + A computed array of approved members + + @property approvedUsers + @type Ember.Array + */ + approvedUsers: mapBy('approvedProjectUsers', 'user'), + + /** + Retuns a subset of at most `MAX_VOLUNTEERS` members from the `approvedUsers` array. + + @property volunteers + @type Ember.Array + */ + volunteers: computed('approvedUsers', function() { + let approvedUsers = get(this, 'approvedUsers'); + + if (approvedUsers.length > MAX_VOLUNTEERS) { + approvedUsers = this.randomSubset(approvedUsers, MAX_VOLUNTEERS); + } + + return approvedUsers; + }), + + /* + * Source: http://stackoverflow.com/a/11935263/1787262 + * Username: Tim Down + * Date: December 3rd, 2016 + */ + randomSubset(arr, size) { + let shuffled = arr.slice(0); + let i = arr.length; + let temp, index; + + while (i--) { + index = Math.floor((i + 1) * Math.random()); + temp = shuffled[index]; + shuffled[index] = shuffled[i]; + shuffled[i] = temp; + } + + return shuffled.slice(0, size); + } }); diff --git a/app/components/volunteer-headshot.js b/app/components/volunteer-headshot.js new file mode 100644 index 000000000..9a41a0fc9 --- /dev/null +++ b/app/components/volunteer-headshot.js @@ -0,0 +1,74 @@ +import Ember from 'ember'; + +const { + Component, + computed, + get, + isPresent +} = Ember; + +/** + The `volunteer-headshot` component presents a thumbnail of a volunteer, their + name and randomly selects one of their roles. + + ## Default Usage + + ```handlebars + {{volunteer-headshot volunteer=user}} + ``` + + @class volunteer-headshot + @module Component + @extends Ember.Component + */ +export default Component.extend({ + attributeBindings: ['data-test-selector'], + classNames: ['volunteer-headshot'], + + /** + A computed alias of the volunteer's user roles. + + @property userRoles + @type Ember.Array + */ + userRoles: computed.alias('volunteer.userRoles'), + + /** + A randomly selected role from the `userRoles` property. + + @property userRole + @type Ember.Model + */ + userRole: computed('userRoles', function() { + let userRoles = get(this, 'userRoles'); + + if (isPresent(userRoles)) { + let randomIndex = Math.floor(Math.random() * get(userRoles, 'length')); + + return userRoles.objectAt(randomIndex); + } + }), + + /** + Returns the volunteer's name. If the `name` property is not defined, it + computes a name from the volunteer's `firstName` & `lastName`. If neither + are defined it returns the volunteer's username. + + @property volunteerName + @type String + */ + volunteerName: computed('volunteer.{name,firstName,lastName}', function() { + let name = get(this, 'volunteer.name'); + let firstName = get(this, 'volunteer.firstName'); + let lastName = get(this, 'volunteer.lastName'); + let userName = get(this, 'volunteer.userName'); + + if (isPresent(name)) { + return name; + } else if (isPresent(firstName) && isPresent(lastName)) { + return `${firstName} ${lastName}`; + } else { + return userName; + } + }) +}); diff --git a/app/models/organization-membership.js b/app/models/organization-membership.js new file mode 100644 index 000000000..71967c8c4 --- /dev/null +++ b/app/models/organization-membership.js @@ -0,0 +1,19 @@ +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { belongsTo } from 'ember-data/relationships'; +import Ember from 'ember'; + +const { computed } = Ember; + +export default Model.extend({ + role: attr(), + + member: belongsTo('user', { async: true }), + organization: belongsTo('organization', { async: true }), + + isAdmin: computed.equal('role', 'admin'), + isContributor: computed.equal('role', 'contributor'), + isNotPending: computed.not('isPending'), + isOwner: computed.equal('role', 'owner'), + isPending: computed.equal('role', 'pending') +}); diff --git a/app/styles/app.scss b/app/styles/app.scss index 2f7839700..6e5d4e1b6 100644 --- a/app/styles/app.scss +++ b/app/styles/app.scss @@ -176,6 +176,7 @@ @import "components/user-projects-list"; @import "components/user-sidebar"; +@import "components/volunteer-headshot"; // used from task/new.hbs @import "components/project-skills-list"; diff --git a/app/styles/components/volunteer-headshot.scss b/app/styles/components/volunteer-headshot.scss new file mode 100644 index 000000000..c0f6c0dad --- /dev/null +++ b/app/styles/components/volunteer-headshot.scss @@ -0,0 +1,22 @@ +.volunteer-headshot { + @include omega(6n); + @include span-columns(2); + + display: inline-block; + margin-bottom: 1em; + + &__name { + font-weight: 600; + margin: 0; + } + + &__role { + font-size: $body-font-size-small; + color: $text--light; + margin-top: 0; + } + + img { + border-radius: 4px; + } +} diff --git a/app/styles/templates/project/thank-you.scss b/app/styles/templates/project/thank-you.scss index 8c1ad068f..b8176881e 100644 --- a/app/styles/templates/project/thank-you.scss +++ b/app/styles/templates/project/thank-you.scss @@ -9,17 +9,27 @@ } } + &__contributors { + @include span-columns(12); + + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + margin: 1.4em 0; + } + h3 { font-size: $header-font-size-large; font-weight: 600; margin: 10px 0; } - p { - margin-bottom: 1.4em; - } - &__message { color: $text--light; + + p { + margin-bottom: 1.4em; + } } } diff --git a/app/templates/components/thank-you-container.hbs b/app/templates/components/thank-you-container.hbs index 841d6e9b3..509850fac 100644 --- a/app/templates/components/thank-you-container.hbs +++ b/app/templates/components/thank-you-container.hbs @@ -14,3 +14,8 @@ {{/if}}

+
+ {{#each volunteers as |volunteer|}} + {{volunteer-headshot volunteer=volunteer data-test-selector="volunteer headshot"}} + {{/each}} +
diff --git a/app/templates/components/volunteer-headshot.hbs b/app/templates/components/volunteer-headshot.hbs new file mode 100644 index 000000000..5b9b7f60b --- /dev/null +++ b/app/templates/components/volunteer-headshot.hbs @@ -0,0 +1,3 @@ +{{volunteerName}} +

{{volunteerName}}

+

{{userRole.role.name}}

diff --git a/tests/integration/components/thank-you-container-test.js b/tests/integration/components/thank-you-container-test.js index b61214b7d..c91801dcb 100644 --- a/tests/integration/components/thank-you-container-test.js +++ b/tests/integration/components/thank-you-container-test.js @@ -1,16 +1,45 @@ import { moduleForComponent, test } from 'ember-qunit'; import hbs from 'htmlbars-inline-precompile'; import PageObject from 'ember-cli-page-object'; -import thankYouContainerComponent from '../../pages/components/thank-you-container'; +import thankYouContainerComponent from 'code-corps-ember/tests/pages/components/thank-you-container'; let page = PageObject.create(thankYouContainerComponent); +const members = [ + 'Rudolph', + 'Charlie Brown', + 'Yukon Cornelius', + 'Frosty the Snowman' +]; + +function generateProjectUsers(size) { + let projectUsers = []; + + for (let i = 0; i < size; i++) { + projectUsers.push({ + role: 'contributor', + user: { + name: members[Math.floor(Math.random() * members.length)], + photoThumbUrl: '/assets/images/icons/test.png', + userRoles: [ + { + name: 'Contributor' + } + ] + } + }); + } + + return projectUsers; +} + moduleForComponent('thank-you-container', 'Integration | Component | thank-you container', { integration: true, beforeEach() { this.set('project', { id: 42, - title: 'A Test Project' + title: 'A Test Project', + projectUsers: generateProjectUsers(14) }); page.setContext(this); @@ -23,3 +52,9 @@ test('it renders the thank you text', function(assert) { assert.equal(page.thankYouText, `From all the volunteers on the ${this.get('project.title')} team.`); }); + +test('it renders a subset of 12 volunteers', function(assert) { + assert.expect(1); + + assert.equal(page.volunteers().count, 12); +}); diff --git a/tests/integration/components/volunteer-headshot-test.js b/tests/integration/components/volunteer-headshot-test.js new file mode 100644 index 000000000..64dbad580 --- /dev/null +++ b/tests/integration/components/volunteer-headshot-test.js @@ -0,0 +1,63 @@ +import { moduleForComponent, test } from 'ember-qunit'; +import hbs from 'htmlbars-inline-precompile'; +import PageObject from 'ember-cli-page-object'; +import volunteerHeadshot from '../../pages/components/volunteer-headshot'; + +let page = PageObject.create(volunteerHeadshot); + +const userRoles = [ + { role: { name: 'Developer' } }, + { role: { name: 'Ember Developer' } }, + { role: { name: 'UX Designer' } }, + { role: { name: 'Software Engineer' } }, + { role: { name: 'Project Coordinator' } }, + { role: { name: 'Designer & Developer' } } +]; + +moduleForComponent('volunteer-headshot', 'Integration | Component | volunteer headshot', { + integration: true, + beforeEach() { + this.set('user', { + name: 'Test User', + photoThumbUrl: '/assets/images/icons/test.png', + userRoles + }); + page.setContext(this); + page.render(hbs`{{volunteer-headshot volunteer=user}}`); + } +}); + +test('it renders the volunteer\'s name', function(assert) { + assert.expect(1); + assert.equal(page.name, this.get('user.name')); +}); + +test('it computes the name if it is not present', function(assert) { + let firstName = 'Split'; + let lastName = 'Name'; + + this.set('user.name', null); + this.set('user.firstName', firstName); + this.set('user.lastName', lastName); + + assert.equal(page.name, `${firstName} ${lastName}`); +}); + +test('it randomly selects one of the available roles', function(assert) { + assert.expect(1); + + let roles = userRoles.map((userRole) => { + return userRole.role.name; + }); + assert.ok(roles.includes(page.role)); +}); + +test('it sets the image alt text', function(assert) { + assert.expect(1); + assert.equal(page.image.alt, `${this.get('user.name')}`); +}); + +test('it sets the image src', function(assert) { + assert.expect(1); + assert.equal(page.image.src, this.get('user.photoThumbUrl')); +}); diff --git a/tests/pages/components/thank-you-container.js b/tests/pages/components/thank-you-container.js index 1ec8b32c6..62001a131 100644 --- a/tests/pages/components/thank-you-container.js +++ b/tests/pages/components/thank-you-container.js @@ -1,5 +1,6 @@ import { clickable, + collection, text } from 'ember-cli-page-object'; @@ -12,5 +13,9 @@ export default { clickLink: clickable('a'), - thankYouText: text('[data-test-selector="thank you message"]') + thankYouText: text('[data-test-selector="thank you message"]'), + + volunteers: collection({ + scope: '[data-test-selector="volunteer headshot"]' + }) }; diff --git a/tests/pages/components/volunteer-headshot.js b/tests/pages/components/volunteer-headshot.js new file mode 100644 index 000000000..d7d99aeaf --- /dev/null +++ b/tests/pages/components/volunteer-headshot.js @@ -0,0 +1,13 @@ +import { attribute, text } from 'ember-cli-page-object'; + +export default { + image: { + scope: 'img', + + alt: attribute('alt'), + src: attribute('src') + }, + + name: text('[data-test-selector="volunteer name"]'), + role: text('[data-test-selector="volunteer role"]') +};