Skip to content

Commit

Permalink
Merge pull request #668 from code-corps/473-new-card-component
Browse files Browse the repository at this point in the history
Add new card component
  • Loading branch information
joshsmith authored Nov 14, 2016
2 parents ddec80e + 5ffb26a commit a8eccb3
Show file tree
Hide file tree
Showing 55 changed files with 1,389 additions and 180 deletions.
21 changes: 21 additions & 0 deletions app/components/donation/card-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Ember from 'ember';

const {
Component,
computed
} = Ember;

export default Component.extend({
classNames: ['card-item'],
classNameBindings: ['isSelected:selected'],

isSelected: computed('card', 'selectedCard', function() {
return this.get('card.id') === this.get('selectedCard.id');
}),

selectedCard: null,

click() {
this.get('select')();
}
});
19 changes: 19 additions & 0 deletions app/components/donation/card-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Ember from 'ember';

const {
Component,
computed: { alias, empty }
} = Ember;

export default Component.extend({
classNames: ['card-list'],

cardNotSelected: empty('selectedCard'),
donationDisabled: alias('cardNotSelected'),

selectedCard: null,

selectCard(card) {
this.set('selectedCard', card);
}
});
84 changes: 81 additions & 3 deletions app/components/donation/credit-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,89 @@ import Ember from 'ember';

const {
Component,
computed: { not }
computed,
computed: { and, not, or },
inject: { service }
} = Ember;

export default Component.extend({
classNames: ['credit-card-form'],
canDonate: true,
cannotDonate: not('canDonate')
month: '',
months: ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12'],
year: '',
years: [],

/**
@property stripe
@type Ember.Service
*/
stripe: service(),

cardInvalid: not('cardValid'),
cardValid: and('isCardNumberValid', 'isCVCValid', 'isExpiryValid'),

preventSubmit: or('isSubmitting', 'cardInvalid'),

isSubmitting: false,

date: computed('month', 'year', function() {
let month = this.get('month');
let year = this.get('year');
return `${month} ${year}`;
}),

isCardNumberValid: computed('cardNumber', function() {
let stripe = this.get('stripe');
let cardNumber = this.get('cardNumber');
return stripe.card.validateCardNumber(cardNumber);
}),

isCVCValid: computed('cvc', function() {
let stripe = this.get('stripe');
let cvc = this.get('cvc');
return stripe.card.validateCVC(cvc);
}),

isExpiryValid: computed('date', function() {
let stripe = this.get('stripe');
let date = this.get('date');
return stripe.card.validateExpiry(date);
}),

init() {
let date = new Date();
let currentMonth = `0${date.getMonth() + 1}`.slice(-2);
this.set('month', currentMonth);
let currentYear = date.getFullYear();
this.set('year', currentYear);
let years = this.generateYears(currentYear);
this.set('years', years);
this._super(...arguments);
},

generateYears(currentYear) {
let years = [];
let endYear = currentYear + 20;
while (endYear >= currentYear) {
years.push(currentYear++);
}
return years;
},

_afterSubmit() {
if (this.get('isDestroyed')) {
return;
}
this.set('isSubmitting', false);
},

actions: {
submit() {
this.set('isSubmitting', true);
let cardAttrs = this.getProperties('cvc', 'cardNumber', 'year', 'month');
let onSubmit = this.get('submit');

onSubmit(cardAttrs).finally(() => this._afterSubmit());
}
}
});
15 changes: 13 additions & 2 deletions app/components/donation/donation-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@ import Ember from 'ember';

const {
Component,
computed: { bool }
computed,
computed: {
and, gt, not
}
} = Ember;

export default Component.extend({
classNames: ['donation-container'],
donationAmount: 0,
projectTitle: null,

canDonate: bool('projectTitle')
canAddCard: computed('hasCards', 'isAddingCard', function() {
let { hasCards, isAddingCard } = this.getProperties('hasCards', 'isAddingCard');
return hasCards ? isAddingCard : true;
}),
canDonate: and('hasCards', 'isNotAddingCard'),
canShowCardList: and('hasCards', 'isNotAddingCard'),
hasCards: gt('cards.length', 0),
hasNoCards: not('hasCards'),
isNotAddingCard: not('isAddingCard')
});
9 changes: 7 additions & 2 deletions app/components/donations/create-donation.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import Ember from 'ember';

const { Component } = Ember;
const { Component, computed, isEmpty } = Ember;

export default Component.extend({
classNames: ['create-donation'],
presetAmounts: [10, 15, 25, 50],
amount: 15
amount: 10,

selectedAmount: computed('amount', 'customAmount', function() {
let { amount, customAmount } = this.getProperties('amount', 'customAmount');
return isEmpty(customAmount) ? amount : customAmount;
})
});
12 changes: 7 additions & 5 deletions app/components/donations/donation-amount-button.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import Ember from 'ember';

const { Component, computed } = Ember;
const { Component, computed, isEmpty } = Ember;

export default Component.extend({
classNames: ['preset-amount'],
classNameBindings: ['presetAmount', 'selected:default:clear'],
tagName: 'button',

selected: computed('presetAmount', 'selectedAmount', function() {
let { presetAmount, selectedAmount } = this.getProperties('presetAmount', 'selectedAmount');
return parseInt(presetAmount) === parseInt(selectedAmount);
selected: computed('customAmount', 'presetAmount', 'selectedAmount', function() {
let { customAmount, presetAmount, selectedAmount } = this.getProperties('customAmount', 'presetAmount', 'selectedAmount');
let amountInPresets = parseInt(presetAmount) === parseInt(selectedAmount);
return isEmpty(customAmount) ? amountInPresets : false;
}),

click() {
this.sendAction('setCustomAmount', null);
let presetAmount = this.get('presetAmount');
this.sendAction('action', presetAmount);
this.sendAction('setAmount', presetAmount);
}
});
86 changes: 81 additions & 5 deletions app/components/error-formatter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Ember from 'ember';

const { Component, computed } = Ember;
const { Component, computed, Object, String } = Ember;

/**
`error-formatter' returns a formatted error message. Place within an 'if'
Expand Down Expand Up @@ -36,9 +36,85 @@ export default Component.extend({
@property messages
@type String
*/
messages: computed('error.errors', function() {
return (this.get('error.errors') || []).map((e) => {
return `${e.title}: ${e.detail}`;
messages: computed('error', function() {
let errorResponse = Object.create(this.get('error'));
let handler = this._findHandler(errorResponse);
if (handler) {
return handler(errorResponse);
}
}),

/**
* Determines the type of error from an error response and returns
* the correct messsage formatter function for it.
*
* @param {Object} errorResponse The error response received from the server
* @return {Function} Function which takes the error response and returns a list of messages
*/
_findHandler(errorResponse) {
if (errorResponse.get('isAdapterError')) {
return this._findHandlerForAdapterError(errorResponse);
} else if (errorResponse.get('error.type') === 'card_error') {
return this._stripeCardErrorMessages;
}
},

/**
* If the error response is determined to be an adapter error, this
* function further determines the type of adapter error and returns
* the correct message formatter for it.
* @param {Object} errorResponse The adapter error response received from the server
* @return {Function} Function which takes the response and returns a list of messages
*/
_findHandlerForAdapterError(errorResponse) {
let payloadContainsValidationErrors = errorResponse.errors.some((error) => error.status === 422);

if (payloadContainsValidationErrors) {
return this._validationErrorMessages;
} else {
return this._adapterErrorMessages;
}
},

/**
* Formats messages for a validation error response
* In most cases, we do not need this, since we do not render those elements outside
* a form. In some cases, however, we are creating a record in the background, so
* we are forced to render those errors separate from a form.
*
* @param {Object} errorResponse The payload received from the server
* @return {Array} An array of string messages
*/
_validationErrorMessages(errorResponse) {
return (errorResponse.get('errors')).map((e) => {
let attributePathElements = e.source.pointer.split('/');
let unformattedAttributeName = attributePathElements[attributePathElements.length - 1];
let attributeName = String.capitalize(unformattedAttributeName);
return `${attributeName} ${e.detail}`;
});
})
},

/**
* Formats messages for an adapter error response.
* An adapter error response contains an array of errors with a
* `title` and a `detail` property, but in most cases, that array only contains
* one error.
* @param {Object} errorResponse Response received from the server
* @return {Array} Array of message strings
*/
_adapterErrorMessages(errorResponse) {
return (errorResponse.get('errors')).map((e) => `${e.title}: ${e.detail}`);
},

/**
* Formats messages for a stripe card error response.
*
* The response contains an `error` object, for which the relevant key is
* the `message` property.
* @param {Object} errorResponse Error response received from the stripe service
* @return {Array} An array of string messages, containing single string
*/
_stripeCardErrorMessages(errorResponse) {
return [errorResponse.get('error.message')];
}
});
Loading

0 comments on commit a8eccb3

Please sign in to comment.