Initial Spark Install
This commit is contained in:
135
spark/resources/assets/js/auth/register-braintree.js
vendored
Normal file
135
spark/resources/assets/js/auth/register-braintree.js
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/braintree'),
|
||||
require('./../mixins/plans'),
|
||||
require('./../mixins/register')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
|
||||
coupon: null,
|
||||
invalidCoupon: false,
|
||||
|
||||
registerForm: $.extend(true, new SparkForm({
|
||||
braintree_type: '',
|
||||
braintree_token: '',
|
||||
plan: '',
|
||||
team: '',
|
||||
team_slug: '',
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
terms: false,
|
||||
coupon: null,
|
||||
invitation: null
|
||||
}), Spark.forms.register)
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the team name for changes.
|
||||
*/
|
||||
'registerForm.team': function (val, oldVal) {
|
||||
if (this.registerForm.team_slug == '' ||
|
||||
this.registerForm.team_slug == oldVal.toLowerCase().replace(/[\s\W-]+/g, '-')
|
||||
) {
|
||||
this.registerForm.team_slug = val.toLowerCase().replace(/[\s\W-]+/g, '-');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getPlans();
|
||||
|
||||
this.query = URI(document.URL).query(true);
|
||||
|
||||
if (this.query.coupon) {
|
||||
this.getCoupon();
|
||||
|
||||
this.registerForm.coupon = this.query.coupon;
|
||||
}
|
||||
|
||||
if (this.query.invitation) {
|
||||
this.getInvitation();
|
||||
|
||||
this.registerForm.invitation = this.query.invitation;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.configureBraintree();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
configureBraintree() {
|
||||
if ( ! Spark.cardUpFront) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.braintree('braintree-container', response => {
|
||||
this.registerForm.braintree_type = response.type;
|
||||
this.registerForm.braintree_token = response.nonce;
|
||||
|
||||
this.register();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the coupon specified in the query string.
|
||||
*/
|
||||
getCoupon() {
|
||||
axios.get('/coupon/' + this.query.coupon)
|
||||
.then(response => {
|
||||
this.coupon = response.data;
|
||||
})
|
||||
.catch(response => {
|
||||
this.invalidCoupon = true;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to register with the application.
|
||||
*/
|
||||
register() {
|
||||
Spark.post('/register', this.registerForm)
|
||||
.then(response => {
|
||||
window.location = response.redirect;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the displayable discount for the coupon.
|
||||
*/
|
||||
discount() {
|
||||
if (this.coupon) {
|
||||
return Vue.filter('currency')(this.coupon.amount_off);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
249
spark/resources/assets/js/auth/register-stripe.js
vendored
Normal file
249
spark/resources/assets/js/auth/register-stripe.js
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/register'),
|
||||
require('./../mixins/plans'),
|
||||
require('./../mixins/vat')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
query: null,
|
||||
|
||||
coupon: null,
|
||||
invalidCoupon: false,
|
||||
|
||||
country: null,
|
||||
taxRate: 0,
|
||||
|
||||
registerForm: $.extend(true, new SparkForm({
|
||||
stripe_token: '',
|
||||
plan: '',
|
||||
team: '',
|
||||
team_slug: '',
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
address: '',
|
||||
address_line_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
country: 'US',
|
||||
vat_id: '',
|
||||
terms: false,
|
||||
coupon: null,
|
||||
invitation: null
|
||||
}), Spark.forms.register),
|
||||
|
||||
cardForm: new SparkForm({
|
||||
name: '',
|
||||
number: '',
|
||||
cvc: '',
|
||||
month: '',
|
||||
year: '',
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch for changes on the entire billing address.
|
||||
*/
|
||||
'currentBillingAddress': function (value) {
|
||||
if ( ! Spark.collectsEuropeanVat) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshTaxRate(this.registerForm);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Watch the team name for changes.
|
||||
*/
|
||||
'registerForm.team': function (val, oldVal) {
|
||||
if (this.registerForm.team_slug == '' ||
|
||||
this.registerForm.team_slug == oldVal.toLowerCase().replace(/[\s\W-]+/g, '-')
|
||||
) {
|
||||
this.registerForm.team_slug = val.toLowerCase().replace(/[\s\W-]+/g, '-');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
Stripe.setPublishableKey(Spark.stripeKey);
|
||||
|
||||
this.getPlans();
|
||||
|
||||
this.guessCountry();
|
||||
|
||||
this.query = URI(document.URL).query(true);
|
||||
|
||||
if (this.query.coupon) {
|
||||
this.getCoupon();
|
||||
|
||||
this.registerForm.coupon = this.query.coupon;
|
||||
}
|
||||
|
||||
if (this.query.invitation) {
|
||||
this.getInvitation();
|
||||
|
||||
this.registerForm.invitation = this.query.invitation;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
//
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Attempt to guess the user's country.
|
||||
*/
|
||||
guessCountry() {
|
||||
axios.get('/geocode/country')
|
||||
.then(response => {
|
||||
if (response.data != 'ZZ') {
|
||||
this.registerForm.country = response.data;
|
||||
}
|
||||
})
|
||||
.catch (response => {
|
||||
//
|
||||
})
|
||||
.finally(function () {
|
||||
this.refreshStatesAndProvinces();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the coupon specified in the query string.
|
||||
*/
|
||||
getCoupon() {
|
||||
axios.get('/coupon/' + this.query.coupon)
|
||||
.then(response => {
|
||||
this.coupon = response.data;
|
||||
})
|
||||
.catch(response => {
|
||||
this.invalidCoupon = true;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Attempt to register with the application.
|
||||
*/
|
||||
register() {
|
||||
this.cardForm.errors.forget();
|
||||
|
||||
this.registerForm.busy = true;
|
||||
this.registerForm.errors.forget();
|
||||
|
||||
if ( ! Spark.cardUpFront || this.selectedPlan.price == 0) {
|
||||
return this.sendRegistration();
|
||||
}
|
||||
|
||||
Stripe.card.createToken(this.stripePayload(), (status, response) => {
|
||||
if (response.error) {
|
||||
this.cardForm.errors.set({number: [response.error.message]})
|
||||
this.registerForm.busy = false;
|
||||
} else {
|
||||
this.registerForm.stripe_token = response.id;
|
||||
this.sendRegistration();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Build the Stripe payload based on the form input.
|
||||
*/
|
||||
stripePayload() {
|
||||
// Here we will build out the payload to send to Stripe to obtain a card token so
|
||||
// we can create the actual subscription. We will build out this data that has
|
||||
// this credit card number, CVC, etc. and exchange it for a secure token ID.
|
||||
return {
|
||||
name: this.cardForm.name,
|
||||
number: this.cardForm.number,
|
||||
cvc: this.cardForm.cvc,
|
||||
exp_month: this.cardForm.month,
|
||||
exp_year: this.cardForm.year,
|
||||
address_line1: this.registerForm.address,
|
||||
address_line2: this.registerForm.address_line_2,
|
||||
address_city: this.registerForm.city,
|
||||
address_state: this.registerForm.state,
|
||||
address_zip: this.registerForm.zip,
|
||||
address_country: this.registerForm.country,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* After obtaining the Stripe token, send the registration to Spark.
|
||||
*/
|
||||
sendRegistration() {
|
||||
Spark.post('/register', this.registerForm)
|
||||
.then(response => {
|
||||
window.location = response.redirect;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Determine if the selected country collects European VAT.
|
||||
*/
|
||||
countryCollectsVat() {
|
||||
return this.collectsVat(this.registerForm.country);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the displayable discount for the coupon.
|
||||
*/
|
||||
discount() {
|
||||
if (this.coupon) {
|
||||
if (this.coupon.percent_off) {
|
||||
return this.coupon.percent_off + '%';
|
||||
} else {
|
||||
return Vue.filter('currency')(this.coupon.amount_off / 100);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the current billing address from the register form.
|
||||
*
|
||||
* This used primarily for wathcing.
|
||||
*/
|
||||
currentBillingAddress() {
|
||||
return this.registerForm.address +
|
||||
this.registerForm.address_line_2 +
|
||||
this.registerForm.city +
|
||||
this.registerForm.state +
|
||||
this.registerForm.zip +
|
||||
this.registerForm.country +
|
||||
this.registerForm.vat_id;
|
||||
}
|
||||
}
|
||||
};
|
69
spark/resources/assets/js/filters.js
vendored
Normal file
69
spark/resources/assets/js/filters.js
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Format the given date.
|
||||
*/
|
||||
Vue.filter('date', value => {
|
||||
return moment.utc(value).local().format('MMMM Do, YYYY')
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Format the given date as a timestamp.
|
||||
*/
|
||||
Vue.filter('datetime', value => {
|
||||
return moment.utc(value).local().format('MMMM Do, YYYY h:mm A');
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Format the given date into a relative time.
|
||||
*/
|
||||
Vue.filter('relative', value => {
|
||||
return moment.utc(value).local().locale('en-short').fromNow();
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Convert the first character to upper case.
|
||||
*
|
||||
* Source: https://github.com/vuejs/vue/blob/1.0/src/filters/index.js#L37
|
||||
*/
|
||||
Vue.filter('capitalize', value => {
|
||||
if (! value && value !== 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return value.toString().charAt(0).toUpperCase()
|
||||
+ value.slice(1);
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Format the given money value.
|
||||
*
|
||||
* Source: https://github.com/vuejs/vue/blob/1.0/src/filters/index.js#L70
|
||||
*/
|
||||
Vue.filter('currency', value => {
|
||||
value = parseFloat(value);
|
||||
|
||||
if (! isFinite(value) || (! value && value !== 0)){
|
||||
return '';
|
||||
}
|
||||
|
||||
var stringified = Math.abs(value).toFixed(2);
|
||||
|
||||
var _int = stringified.slice(0, -1 - 2);
|
||||
|
||||
var i = _int.length % 3;
|
||||
|
||||
var head = i > 0
|
||||
? (_int.slice(0, i) + (_int.length > 3 ? ',' : ''))
|
||||
: '';
|
||||
|
||||
var _float = stringified.slice(-1 - 2);
|
||||
|
||||
var sign = value < 0 ? '-' : '';
|
||||
|
||||
return sign + window.Spark.currencySymbol + head +
|
||||
_int.slice(i).replace(/(\d{3})(?=\d)/g, '$1,') +
|
||||
_float;
|
||||
});
|
23
spark/resources/assets/js/forms/bootstrap.js
vendored
Normal file
23
spark/resources/assets/js/forms/bootstrap.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Initialize the Spark form extension points.
|
||||
*/
|
||||
Spark.forms = {
|
||||
register: {},
|
||||
updateContactInformation: {},
|
||||
updateTeamMember: {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the SparkForm helper class.
|
||||
*/
|
||||
require('./form');
|
||||
|
||||
/**
|
||||
* Define the SparkFormError collection class.
|
||||
*/
|
||||
require('./errors');
|
||||
|
||||
/**
|
||||
* Add additional HTTP / form helpers to the Spark object.
|
||||
*/
|
||||
$.extend(Spark, require('./http'));
|
71
spark/resources/assets/js/forms/errors.js
vendored
Normal file
71
spark/resources/assets/js/forms/errors.js
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Spark form error collection class.
|
||||
*/
|
||||
window.SparkFormErrors = function () {
|
||||
this.errors = {};
|
||||
|
||||
/**
|
||||
* Determine if the collection has any errors.
|
||||
*/
|
||||
this.hasErrors = function () {
|
||||
return ! _.isEmpty(this.errors);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the collection has errors for a given field.
|
||||
*/
|
||||
this.has = function (field) {
|
||||
return _.indexOf(_.keys(this.errors), field) > -1;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the raw errors for the collection.
|
||||
*/
|
||||
this.all = function () {
|
||||
return this.errors;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the errors for the collection in a flat array.
|
||||
*/
|
||||
this.flatten = function () {
|
||||
return _.flatten(_.toArray(this.errors));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the first error message for a given field.
|
||||
*/
|
||||
this.get = function (field) {
|
||||
if (this.has(field)) {
|
||||
return this.errors[field][0];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the raw errors for the collection.
|
||||
*/
|
||||
this.set = function (errors) {
|
||||
if (typeof errors === 'object') {
|
||||
this.errors = errors;
|
||||
} else {
|
||||
this.errors = {'form': ['Something went wrong. Please try again or contact customer support.']};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Remove errors from the collection.
|
||||
*/
|
||||
this.forget = function (field) {
|
||||
if (typeof field === 'undefined') {
|
||||
this.errors = {};
|
||||
} else {
|
||||
Vue.delete(this.errors, field);
|
||||
}
|
||||
};
|
||||
};
|
51
spark/resources/assets/js/forms/form.js
vendored
Normal file
51
spark/resources/assets/js/forms/form.js
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* SparkForm helper class. Used to set common properties on all forms.
|
||||
*/
|
||||
window.SparkForm = function (data) {
|
||||
var form = this;
|
||||
|
||||
$.extend(this, data);
|
||||
|
||||
/**
|
||||
* Create the form error helper instance.
|
||||
*/
|
||||
this.errors = new SparkFormErrors();
|
||||
|
||||
this.busy = false;
|
||||
this.successful = false;
|
||||
|
||||
/**
|
||||
* Start processing the form.
|
||||
*/
|
||||
this.startProcessing = function () {
|
||||
form.errors.forget();
|
||||
form.busy = true;
|
||||
form.successful = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Finish processing the form.
|
||||
*/
|
||||
this.finishProcessing = function () {
|
||||
form.busy = false;
|
||||
form.successful = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset the errors and other state for the form.
|
||||
*/
|
||||
this.resetStatus = function () {
|
||||
form.errors.forget();
|
||||
form.busy = false;
|
||||
form.successful = false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the errors on the form.
|
||||
*/
|
||||
this.setErrors = function (errors) {
|
||||
form.busy = false;
|
||||
form.errors.set(errors);
|
||||
};
|
||||
};
|
56
spark/resources/assets/js/forms/http.js
vendored
Normal file
56
spark/resources/assets/js/forms/http.js
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* Helper method for making POST HTTP requests.
|
||||
*/
|
||||
post(uri, form) {
|
||||
return Spark.sendForm('post', uri, form);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Helper method for making PUT HTTP requests.
|
||||
*/
|
||||
put(uri, form) {
|
||||
return Spark.sendForm('put', uri, form);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Helper method for making PATCH HTTP requests.
|
||||
*/
|
||||
patch(uri, form) {
|
||||
return Spark.sendForm('patch', uri, form);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Helper method for making DELETE HTTP requests.
|
||||
*/
|
||||
delete(uri, form) {
|
||||
return Spark.sendForm('delete', uri, form);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Send the form to the back-end server.
|
||||
*
|
||||
* This function will clear old errors, update "busy" status, etc.
|
||||
*/
|
||||
sendForm(method, uri, form) {
|
||||
return new Promise((resolve, reject) => {
|
||||
form.startProcessing();
|
||||
|
||||
axios[method](uri, JSON.parse(JSON.stringify(form)))
|
||||
.then(response => {
|
||||
form.finishProcessing();
|
||||
|
||||
resolve(response.data);
|
||||
})
|
||||
.catch(errors => {
|
||||
form.setErrors(errors.response.data.errors);
|
||||
|
||||
reject(errors.response.data);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
27
spark/resources/assets/js/interceptors.js
vendored
Normal file
27
spark/resources/assets/js/interceptors.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
module.exports = (request, next) => {
|
||||
|
||||
if (Cookies.get('XSRF-TOKEN') !== undefined) {
|
||||
request.headers.set('X-XSRF-TOKEN', Cookies.get('XSRF-TOKEN'));
|
||||
}
|
||||
|
||||
request.headers.set('X-CSRF-TOKEN', Spark.csrfToken);
|
||||
|
||||
/**
|
||||
* Intercept the incoming responses.
|
||||
*
|
||||
* Handle any unexpected HTTP errors and pop up modals, etc.
|
||||
*/
|
||||
next(response => {
|
||||
switch (response.status) {
|
||||
case 401:
|
||||
Vue.http.get('/logout');
|
||||
$('#modal-session-expired').modal('show');
|
||||
break;
|
||||
|
||||
case 402:
|
||||
window.location = '/settings#/subscription';
|
||||
break;
|
||||
}
|
||||
|
||||
});
|
||||
};
|
65
spark/resources/assets/js/kiosk/add-discount.js
vendored
Normal file
65
spark/resources/assets/js/kiosk/add-discount.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
function kioskAddDiscountForm () {
|
||||
return {
|
||||
type: 'amount',
|
||||
value: null,
|
||||
duration: 'once',
|
||||
months: null
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mixins: [require('./../mixins/discounts')],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
loadingCurrentDiscount: false,
|
||||
currentDiscount: null,
|
||||
|
||||
discountingUser: null,
|
||||
form: new SparkForm(kioskAddDiscountForm())
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
Bus.$on('addDiscount', function (user) {
|
||||
self.form = new SparkForm(kioskAddDiscountForm());
|
||||
|
||||
self.setUser(user);
|
||||
|
||||
$('#modal-add-discount').modal('show');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Set the user receiving teh discount.
|
||||
*/
|
||||
setUser(user) {
|
||||
this.discountingUser = user;
|
||||
|
||||
this.getCurrentDiscountForUser(user);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Apply the discount to the user.
|
||||
*/
|
||||
applyDiscount() {
|
||||
Spark.post('/spark/kiosk/users/discount/' + this.discountingUser.id, this.form)
|
||||
.then(() => {
|
||||
$('#modal-add-discount').modal('hide');
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
116
spark/resources/assets/js/kiosk/announcements.js
vendored
Normal file
116
spark/resources/assets/js/kiosk/announcements.js
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
var announcementsCreateForm = function () {
|
||||
return {
|
||||
body: '',
|
||||
action_text: '',
|
||||
action_url: ''
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
announcements: [],
|
||||
updatingAnnouncement: null,
|
||||
deletingAnnouncement: null,
|
||||
|
||||
createForm: new SparkForm(announcementsCreateForm()),
|
||||
updateForm: new SparkForm(announcementsCreateForm()),
|
||||
|
||||
deleteForm: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
Bus.$on('sparkHashChanged', function (hash, parameters) {
|
||||
if (hash == 'announcements' && self.announcements.length === 0) {
|
||||
self.getAnnouncements();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get all of the announcements.
|
||||
*/
|
||||
getAnnouncements() {
|
||||
axios.get('/spark/kiosk/announcements')
|
||||
.then(response => {
|
||||
this.announcements = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Create a new announcement.
|
||||
*/
|
||||
create() {
|
||||
Spark.post('/spark/kiosk/announcements', this.createForm)
|
||||
.then(() => {
|
||||
this.createForm = new SparkForm(announcementsCreateForm());
|
||||
|
||||
this.getAnnouncements();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Edit the given announcement.
|
||||
*/
|
||||
editAnnouncement(announcement) {
|
||||
this.updatingAnnouncement = announcement;
|
||||
|
||||
this.updateForm.icon = announcement.icon;
|
||||
this.updateForm.body = announcement.body;
|
||||
this.updateForm.action_text = announcement.action_text;
|
||||
this.updateForm.action_url = announcement.action_url;
|
||||
|
||||
$('#modal-update-announcement').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the specified announcement.
|
||||
*/
|
||||
update() {
|
||||
Spark.put('/spark/kiosk/announcements/' + this.updatingAnnouncement.id, this.updateForm)
|
||||
.then(() => {
|
||||
this.getAnnouncements();
|
||||
|
||||
$('#modal-update-announcement').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the approval dialog for deleting an announcement.
|
||||
*/
|
||||
approveAnnouncementDelete(announcement) {
|
||||
this.deletingAnnouncement = announcement;
|
||||
|
||||
$('#modal-delete-announcement').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Delete the specified announcement.
|
||||
*/
|
||||
deleteAnnouncement() {
|
||||
Spark.delete('/spark/kiosk/announcements/' + this.deletingAnnouncement.id, this.deleteForm)
|
||||
.then(() => {
|
||||
this.getAnnouncements();
|
||||
|
||||
$('#modal-delete-announcement').modal('hide');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
33
spark/resources/assets/js/kiosk/kiosk.js
vendored
Normal file
33
spark/resources/assets/js/kiosk/kiosk.js
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [require('./../mixins/tab-state')],
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.usePushStateForTabs('.spark-settings-tabs');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
Bus.$on('sparkHashChanged', function (hash, parameters) {
|
||||
if (hash == 'users') {
|
||||
setTimeout(() => {
|
||||
$('#kiosk-users-search').focus();
|
||||
}, 150);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
};
|
294
spark/resources/assets/js/kiosk/metrics.js
vendored
Normal file
294
spark/resources/assets/js/kiosk/metrics.js
vendored
Normal file
@@ -0,0 +1,294 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
monthlyRecurringRevenue: 0,
|
||||
yearlyRecurringRevenue: 0,
|
||||
totalVolume: 0,
|
||||
genericTrialUsers: 0,
|
||||
|
||||
indicators: [],
|
||||
lastMonthsIndicators: null,
|
||||
lastYearsIndicators: null,
|
||||
|
||||
plans: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
Bus.$on('sparkHashChanged', function (hash, parameters) {
|
||||
if (hash == 'metrics' && self.yearlyRecurringRevenue === 0) {
|
||||
self.getRevenue();
|
||||
self.getPlans();
|
||||
self.getTrialUsers();
|
||||
self.getPerformanceIndicators();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the revenue information for the application.
|
||||
*/
|
||||
getRevenue() {
|
||||
axios.get('/spark/kiosk/performance-indicators/revenue')
|
||||
.then(response => {
|
||||
this.yearlyRecurringRevenue = response.data.yearlyRecurringRevenue;
|
||||
this.monthlyRecurringRevenue = response.data.monthlyRecurringRevenue;
|
||||
this.totalVolume = response.data.totalVolume;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the subscriber information for the application.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/kiosk/performance-indicators/plans')
|
||||
.then(response => {
|
||||
this.plans = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the number of users that are on a generic trial.
|
||||
*/
|
||||
getTrialUsers() {
|
||||
axios.get('/spark/kiosk/performance-indicators/trialing')
|
||||
.then(response => {
|
||||
this.genericTrialUsers = parseInt(response.data);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the performance indicators for the application.
|
||||
*/
|
||||
getPerformanceIndicators() {
|
||||
axios.get('/spark/kiosk/performance-indicators')
|
||||
.then(response => {
|
||||
this.indicators = response.data.indicators;
|
||||
this.lastMonthsIndicators = response.data.last_month;
|
||||
this.lastYearsIndicators = response.data.last_year;
|
||||
|
||||
Vue.nextTick(() => {
|
||||
this.drawCharts();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the performance indicator charts.
|
||||
*/
|
||||
drawCharts() {
|
||||
this.drawMonthlyRecurringRevenueChart();
|
||||
this.drawYearlyRecurringRevenueChart();
|
||||
this.drawDailyVolumeChart();
|
||||
this.drawNewUsersChart();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the monthly recurring revenue chart.
|
||||
*/
|
||||
drawMonthlyRecurringRevenueChart() {
|
||||
return this.drawCurrencyChart(
|
||||
'monthlyRecurringRevenueChart', 30, indicator => indicator.monthly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the yearly recurring revenue chart.
|
||||
*/
|
||||
drawYearlyRecurringRevenueChart() {
|
||||
return this.drawCurrencyChart(
|
||||
'yearlyRecurringRevenueChart', 30, indicator => indicator.yearly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the daily volume chart.
|
||||
*/
|
||||
drawDailyVolumeChart() {
|
||||
return this.drawCurrencyChart(
|
||||
'dailyVolumeChart', 14, indicator => indicator.daily_volume
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw the daily new users chart.
|
||||
*/
|
||||
drawNewUsersChart() {
|
||||
return this.drawChart(
|
||||
'newUsersChart', 14, indicator => indicator.new_users
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw a chart with currency formatting on the Y-Axis.
|
||||
*/
|
||||
drawCurrencyChart(id, days, dataGatherer) {
|
||||
return this.drawChart(id, days, dataGatherer, value =>
|
||||
Vue.filter('currency')(value.value)
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Draw a chart with the given parameters.
|
||||
*/
|
||||
drawChart(id, days, dataGatherer, scaleLabelFormatter) {
|
||||
var dataset = JSON.parse(JSON.stringify(this.baseChartDataSet));
|
||||
|
||||
dataset.data = _.map(_.last(this.indicators, days), dataGatherer);
|
||||
|
||||
// Here we will build out the dataset for the chart. This will contain the dates and data
|
||||
// points for the chart. Each chart on the Kiosk only gets one dataset so we only need
|
||||
// to add it a single element to this array here. But, charts could have more later.
|
||||
var data = {
|
||||
labels: _.last(this.availableChartDates, days),
|
||||
datasets: [dataset]
|
||||
};
|
||||
|
||||
var options = { responsive: true };
|
||||
|
||||
// If a scale label formatter was passed, we will hand that to this chart library to fill
|
||||
// out the Y-Axis labels. This is particularly useful when we want to format them as a
|
||||
// currency as we do on all of our revenue charts that we display on the Kiosk here.
|
||||
if (arguments.length === 4) {
|
||||
options.scaleLabel = scaleLabelFormatter;
|
||||
}
|
||||
|
||||
var chart = new Chart(document.getElementById(id).getContext('2d'), {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the percent change between two numbers.
|
||||
*/
|
||||
percentChange(current, previous) {
|
||||
var change = Math.round(((current - previous) / previous) * 100);
|
||||
|
||||
return change > 0 ? '+' + change.toFixed(0) : change.toFixed(0);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Calculate the monthly change in monthly recurring revenue.
|
||||
*/
|
||||
monthlyChangeInMonthlyRecurringRevenue() {
|
||||
if ( ! this.lastMonthsIndicators || ! this.indicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.percentChange(
|
||||
_.last(this.indicators).monthly_recurring_revenue,
|
||||
this.lastMonthsIndicators.monthly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the yearly change in monthly recurring revenue.
|
||||
*/
|
||||
yearlyChangeInMonthlyRecurringRevenue() {
|
||||
if ( ! this.lastYearsIndicators || ! this.indicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.percentChange(
|
||||
_.last(this.indicators).monthly_recurring_revenue,
|
||||
this.lastYearsIndicators.monthly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the monthly change in yearly recurring revenue.
|
||||
*/
|
||||
monthlyChangeInYearlyRecurringRevenue() {
|
||||
if ( ! this.lastMonthsIndicators || ! this.indicators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.percentChange(
|
||||
_.last(this.indicators).yearly_recurring_revenue,
|
||||
this.lastMonthsIndicators.yearly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the yearly change in yearly recurring revenue.
|
||||
*/
|
||||
yearlyChangeInYearlyRecurringRevenue() {
|
||||
if ( ! this.lastYearsIndicators || ! this.indicators) {
|
||||
return false;
|
||||
}
|
||||
;
|
||||
return this.percentChange(
|
||||
_.last(this.indicators).yearly_recurring_revenue,
|
||||
this.lastYearsIndicators.yearly_recurring_revenue
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the total number of users trialing.
|
||||
*/
|
||||
totalTrialUsers() {
|
||||
return this.genericTrialUsers + _.reduce(this.plans, (memo, plan) => {
|
||||
return memo + plan.trialing;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the available, formatted chart dates for the current indicators.
|
||||
*/
|
||||
availableChartDates() {
|
||||
return _.map(this.indicators, indicator => {
|
||||
return moment(indicator.created_at).format('M/D');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the base chart data set.
|
||||
*/
|
||||
baseChartDataSet() {
|
||||
return {
|
||||
label: "Dataset",
|
||||
fillColor: "rgba(151,187,205,0.2)",
|
||||
strokeColor: "rgba(151,187,205,1)",
|
||||
pointColor: "rgba(151,187,205,1)",
|
||||
pointStrokeColor: "#fff",
|
||||
pointHighlightFill: "#fff",
|
||||
pointHighlightStroke: "rgba(151,187,205,1)",
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
148
spark/resources/assets/js/kiosk/profile.js
vendored
Normal file
148
spark/resources/assets/js/kiosk/profile.js
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
module.exports = {
|
||||
props: ['user', 'plans'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
profile: null,
|
||||
revenue: 0
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$parent.$on('showUserProfile', function(id) {
|
||||
self.getUserProfile(id);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
Mousetrap.bind('esc', e => this.showSearch());
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the profile user.
|
||||
*/
|
||||
getUserProfile(id) {
|
||||
this.loading = true;
|
||||
|
||||
axios.get('/spark/kiosk/users/' + id + '/profile')
|
||||
.then(response => {
|
||||
this.profile = response.data.user;
|
||||
this.revenue = response.data.revenue;
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Impersonate the given user.
|
||||
*/
|
||||
impersonate(user) {
|
||||
window.location = '/spark/kiosk/users/impersonate/' + user.id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the discount modal for the given user.
|
||||
*/
|
||||
addDiscount(user) {
|
||||
Bus.$emit('addDiscount', user);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the plan the user is actively subscribed to.
|
||||
*/
|
||||
activePlan(billable) {
|
||||
if (this.activeSubscription(billable)) {
|
||||
var activeSubscription = this.activeSubscription(billable);
|
||||
|
||||
return _.find(this.plans, (plan) => {
|
||||
return plan.id == activeSubscription.provider_plan;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the active, valid subscription for the user.
|
||||
*/
|
||||
activeSubscription(billable) {
|
||||
var subscription = this.subscription(billable);
|
||||
|
||||
if ( ! subscription ||
|
||||
(subscription.ends_at &&
|
||||
moment.utc().isAfter(moment.utc(subscription.ends_at)))) {
|
||||
return;
|
||||
}
|
||||
|
||||
return subscription;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the active subscription instance.
|
||||
*/
|
||||
subscription(billable) {
|
||||
if ( ! billable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = _.find(
|
||||
billable.subscriptions,
|
||||
subscription => subscription.name == 'default'
|
||||
);
|
||||
|
||||
if (typeof subscription !== 'undefined') {
|
||||
return subscription;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the customer URL on the billing provider's website.
|
||||
*/
|
||||
customerUrlOnBillingProvider(billable) {
|
||||
if (! billable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.spark.usesStripe) {
|
||||
return 'https://dashboard.stripe.com/customers/' + billable.stripe_id;
|
||||
} else {
|
||||
var domain = Spark.env == 'production' ? '' : 'sandbox.';
|
||||
|
||||
return 'https://' + domain + 'braintreegateway.com/merchants/' +
|
||||
Spark.braintreeMerchantId +
|
||||
'/customers/' +
|
||||
billable.braintree_id;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the search results and hide the user profile.
|
||||
*/
|
||||
showSearch() {
|
||||
this.$parent.$emit('showSearch');
|
||||
|
||||
this.profile = null;
|
||||
}
|
||||
}
|
||||
};
|
123
spark/resources/assets/js/kiosk/users.js
vendored
Normal file
123
spark/resources/assets/js/kiosk/users.js
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
plans: [],
|
||||
|
||||
searchForm: new SparkForm({
|
||||
query: ''
|
||||
}),
|
||||
|
||||
searching: false,
|
||||
noSearchResults: false,
|
||||
searchResults: [],
|
||||
|
||||
showingUserProfile: false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.getPlans();
|
||||
|
||||
this.$on('showSearch', function(){
|
||||
self.navigateToSearch();
|
||||
});
|
||||
|
||||
Bus.$on('sparkHashChanged', function (hash, parameters) {
|
||||
if (hash != 'users') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parameters && parameters.length > 0) {
|
||||
self.loadProfile({ id: parameters[0] });
|
||||
} else {
|
||||
self.showSearch();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get all of the available subscription plans.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/plans')
|
||||
.then(response => {
|
||||
this.plans = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Perform a search for the given query.
|
||||
*/
|
||||
search() {
|
||||
this.searching = true;
|
||||
this.noSearchResults = false;
|
||||
|
||||
axios.post('/spark/kiosk/users/search', this.searchForm)
|
||||
.then(response => {
|
||||
this.searchResults = response.data;
|
||||
this.noSearchResults = this.searchResults.length === 0;
|
||||
|
||||
this.searching = false;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the search results and update the browser history.
|
||||
*/
|
||||
navigateToSearch() {
|
||||
history.pushState(null, null, '#/users');
|
||||
|
||||
this.showSearch();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the search results.
|
||||
*/
|
||||
showSearch() {
|
||||
this.showingUserProfile = false;
|
||||
|
||||
Vue.nextTick(function () {
|
||||
$('#kiosk-users-search').focus();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the user profile for the given user.
|
||||
*/
|
||||
showUserProfile(user) {
|
||||
history.pushState(null, null, '#/users/' + user.id);
|
||||
|
||||
this.loadProfile(user);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Load the user profile for the given user.
|
||||
*/
|
||||
loadProfile(user) {
|
||||
this.$emit('showUserProfile', user.id);
|
||||
|
||||
this.showingUserProfile = true;
|
||||
}
|
||||
}
|
||||
};
|
30
spark/resources/assets/js/mixin.js
vendored
Normal file
30
spark/resources/assets/js/mixin.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
module.exports = {
|
||||
computed: {
|
||||
/**
|
||||
* Get the billable entity.
|
||||
*/
|
||||
billable() {
|
||||
if (this.billableType) {
|
||||
return this.billableType == 'user' ? this.user : this.team;
|
||||
} else {
|
||||
return this.user;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the current billable entity is a user.
|
||||
*/
|
||||
billingUser() {
|
||||
return this.billableType && this.billableType == 'user';
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Access the global Spark object.
|
||||
*/
|
||||
spark() {
|
||||
return window.Spark;
|
||||
}
|
||||
}
|
||||
};
|
38
spark/resources/assets/js/mixins/braintree.js
vendored
Normal file
38
spark/resources/assets/js/mixins/braintree.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
window.braintreeCheckout = [];
|
||||
|
||||
module.exports = {
|
||||
methods: {
|
||||
/**
|
||||
* Configure the Braintree container.
|
||||
*/
|
||||
braintree(containerName, callback) {
|
||||
braintree.setup(Spark.braintreeToken, 'dropin', {
|
||||
container: containerName,
|
||||
paypal: {
|
||||
singleUse: false,
|
||||
locale: 'en_us',
|
||||
enableShippingAddress: false
|
||||
},
|
||||
dataCollector: {
|
||||
paypal: true
|
||||
},
|
||||
onReady(checkout) {
|
||||
window.braintreeCheckout[containerName] = checkout;
|
||||
},
|
||||
onPaymentMethodReceived: callback
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Reset the Braintree container.
|
||||
*/
|
||||
resetBraintree(containerName, callback) {
|
||||
window.braintreeCheckout[containerName].teardown(() => {
|
||||
window.braintreeCheckout[containerName] = null;
|
||||
|
||||
this.braintree(containerName, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
96
spark/resources/assets/js/mixins/discounts.js
vendored
Normal file
96
spark/resources/assets/js/mixins/discounts.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
module.exports = {
|
||||
methods: {
|
||||
/**
|
||||
* Get the current discount for the given billable entity.
|
||||
*/
|
||||
getCurrentDiscountForBillable(type, billable) {
|
||||
if (type === 'user') {
|
||||
return this.getCurrentDiscountForUser(billable);
|
||||
} else {
|
||||
return this.getCurrentDiscountForTeam(billable);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current discount for the user.
|
||||
*/
|
||||
getCurrentDiscountForUser(user) {
|
||||
this.currentDiscount = null;
|
||||
|
||||
this.loadingCurrentDiscount = true;
|
||||
|
||||
axios.get(`/coupon/user/${user.id}`)
|
||||
.then(response => {
|
||||
if (response.status == 200) {
|
||||
this.currentDiscount = response.data;
|
||||
}
|
||||
|
||||
this.loadingCurrentDiscount = false;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the current discount for the team.
|
||||
*/
|
||||
getCurrentDiscountForTeam(team) {
|
||||
this.currentDiscount = null;
|
||||
|
||||
this.loadingCurrentDiscount = true;
|
||||
|
||||
axios.get(`/coupon/${Spark.teamString}/${team.id}`)
|
||||
.then(response => {
|
||||
if (response.status == 200) {
|
||||
this.currentDiscount = response.data;
|
||||
}
|
||||
|
||||
this.loadingCurrentDiscount = false;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the formatted discount amount for the given discount.
|
||||
*/
|
||||
formattedDiscount(discount) {
|
||||
if ( ! discount) {
|
||||
return
|
||||
}
|
||||
|
||||
if (discount.percent_off) {
|
||||
return `${discount.percent_off}%`;
|
||||
} else {
|
||||
return Vue.filter('currency')(
|
||||
this.calculateAmountOff(discount.amount_off)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the amount off for the given discount amount.
|
||||
*/
|
||||
calculateAmountOff(amount) {
|
||||
return amount / 100;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the formatted discount duration for the given discount.
|
||||
*/
|
||||
formattedDiscountDuration(discount) {
|
||||
if ( ! discount) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (discount.duration) {
|
||||
case 'forever':
|
||||
return 'all future invoices';
|
||||
case 'once':
|
||||
return 'a single invoice';
|
||||
case 'repeating':
|
||||
return `all invoices during the next ${discount.duration_in_months} months`;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
140
spark/resources/assets/js/mixins/plans.js
vendored
Normal file
140
spark/resources/assets/js/mixins/plans.js
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* This mixin is primarily used to share code for the "interval" selector
|
||||
* on the registration and subscription screens, which is used to show
|
||||
* all of the various subscription plans offered by the application.
|
||||
*/
|
||||
module.exports = {
|
||||
data() {
|
||||
return {
|
||||
selectedPlan: null,
|
||||
detailingPlan: null,
|
||||
|
||||
showingMonthlyPlans: true,
|
||||
showingYearlyPlans: false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Switch to showing monthly plans.
|
||||
*/
|
||||
showMonthlyPlans() {
|
||||
this.showingMonthlyPlans = true;
|
||||
|
||||
this.showingYearlyPlans = false;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Switch to showing yearly plans.
|
||||
*/
|
||||
showYearlyPlans() {
|
||||
this.showingMonthlyPlans = false;
|
||||
|
||||
this.showingYearlyPlans = true;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the plan details for the given plan.
|
||||
*/
|
||||
showPlanDetails(plan) {
|
||||
this.detailingPlan = plan;
|
||||
|
||||
$('#modal-plan-details').modal('show');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the active "interval" being displayed.
|
||||
*/
|
||||
activeInterval() {
|
||||
return this.showingMonthlyPlans ? 'monthly' : 'yearly';
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the plans for the active interval.
|
||||
*/
|
||||
plansForActiveInterval() {
|
||||
return _.filter(this.plans, plan => {
|
||||
return plan.active && (plan.price == 0 || plan.interval == this.activeInterval);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the paid plans.
|
||||
*/
|
||||
paidPlans() {
|
||||
return _.filter(this.plans, plan => {
|
||||
return plan.active && plan.price > 0;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the paid plans for the active interval.
|
||||
*/
|
||||
paidPlansForActiveInterval() {
|
||||
return _.filter(this.plansForActiveInterval, plan => {
|
||||
return plan.active && plan.price > 0;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if both monthly and yearly plans are available.
|
||||
*/
|
||||
hasMonthlyAndYearlyPlans() {
|
||||
return this.monthlyPlans.length > 0 && this.yearlyPlans.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if both monthly and yearly plans are available.
|
||||
*/
|
||||
hasMonthlyAndYearlyPaidPlans() {
|
||||
return _.where(this.paidPlans, {interval: 'monthly'}).length > 0 &&
|
||||
_.where(this.paidPlans, {interval: 'yearly'}).length > 0;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if only yearly plans are available.
|
||||
*/
|
||||
onlyHasYearlyPlans() {
|
||||
return this.monthlyPlans.length == 0 && this.yearlyPlans.length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if both monthly and yearly plans are available.
|
||||
*/
|
||||
onlyHasYearlyPaidPlans() {
|
||||
return _.where(this.paidPlans, {interval: 'monthly'}).length == 0 &&
|
||||
_.where(this.paidPlans, {interval: 'yearly'}).length > 0;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the monthly plans.
|
||||
*/
|
||||
monthlyPlans() {
|
||||
return _.filter(this.plans, plan => {
|
||||
return plan.active && plan.interval == 'monthly';
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the yearly plans.
|
||||
*/
|
||||
yearlyPlans() {
|
||||
return _.filter(this.plans, plan => {
|
||||
return plan.active && plan.interval == 'yearly';
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
138
spark/resources/assets/js/mixins/register.js
vendored
Normal file
138
spark/resources/assets/js/mixins/register.js
vendored
Normal file
@@ -0,0 +1,138 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* The mixin's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
plans: [],
|
||||
selectedPlan: null,
|
||||
|
||||
invitation: null,
|
||||
invalidInvitation: false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the active plans for the application.
|
||||
*/
|
||||
getPlans() {
|
||||
if ( ! Spark.cardUpFront) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.get('/spark/plans')
|
||||
.then(response => {
|
||||
var plans = response.data;
|
||||
|
||||
this.plans = _.where(plans, {type: "user"}).length > 0
|
||||
? _.where(plans, {type: "user"})
|
||||
: _.where(plans, {type: "team"});
|
||||
|
||||
this.selectAppropriateDefaultPlan();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the invitation specified in the query string.
|
||||
*/
|
||||
getInvitation() {
|
||||
axios.get(`/invitations/${this.query.invitation}`)
|
||||
.then(response => {
|
||||
this.invitation = response.data;
|
||||
})
|
||||
.catch(response => {
|
||||
this.invalidInvitation = true;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Select the appropriate default plan for registration.
|
||||
*/
|
||||
selectAppropriateDefaultPlan() {
|
||||
if (this.query.plan) {
|
||||
this.selectPlanById(this.query.plan) || this.selectPlanByName(this.query.plan);
|
||||
} else if (this.query.invitation) {
|
||||
this.selectFreePlan();
|
||||
} else if (this.paidPlansForActiveInterval.length > 0) {
|
||||
this.selectPlan(this.paidPlansForActiveInterval[0]);
|
||||
} else {
|
||||
this.selectFreePlan();
|
||||
}
|
||||
|
||||
if (this.shouldShowYearlyPlans()) {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Select the free plan.
|
||||
*/
|
||||
selectFreePlan() {
|
||||
const plan = _.find(this.plans, plan => plan.price === 0);
|
||||
|
||||
if (typeof plan !== 'undefined') {
|
||||
this.selectPlan(plan);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Select the plan with the given id.
|
||||
*/
|
||||
selectPlanById(id) {
|
||||
_.each(this.plans, plan => {
|
||||
if (plan.id == id) {
|
||||
this.selectPlan(plan);
|
||||
}
|
||||
});
|
||||
|
||||
return this.selectedPlan;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Select the plan with the given name.
|
||||
*/
|
||||
selectPlanByName(name) {
|
||||
_.each(this.plans, plan => {
|
||||
if (plan.name == name) {
|
||||
this.selectPlan(plan);
|
||||
}
|
||||
});
|
||||
|
||||
return this.selectedPlan;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the given plan is selected.
|
||||
*/
|
||||
isSelected(plan) {
|
||||
return this.selectedPlan && plan.id == this.selectedPlan.id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Select the given plan.
|
||||
*/
|
||||
selectPlan(plan) {
|
||||
this.selectedPlan = plan;
|
||||
|
||||
this.registerForm.plan = plan.id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if we should show the yearly plans.
|
||||
*/
|
||||
shouldShowYearlyPlans(){
|
||||
return (this.monthlyPlans.length == 0 && this.yearlyPlans.length > 0) ||
|
||||
this.selectedPlan.interval == 'yearly'
|
||||
}
|
||||
}
|
||||
};
|
179
spark/resources/assets/js/mixins/subscriptions.js
vendored
Normal file
179
spark/resources/assets/js/mixins/subscriptions.js
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* This mixin is used by most of the subscription related screens to select plans
|
||||
* and send subscription plan changes to the server. This contains helpers for
|
||||
* the active subscription, trial information and other convenience helpers.
|
||||
*/
|
||||
module.exports = {
|
||||
/**
|
||||
* The mixin's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
selectingPlan: null,
|
||||
|
||||
planForm: new SparkForm({})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the subscription to the given plan.
|
||||
*
|
||||
* Used when updating or resuming the subscription plan.
|
||||
*/
|
||||
updateSubscription(plan) {
|
||||
this.selectingPlan = plan;
|
||||
|
||||
this.planForm.errors.forget();
|
||||
|
||||
// Here we will send the request to the server to update the subscription plan and
|
||||
// update the user and team once the request is complete. This method gets used
|
||||
// for both updating subscriptions plus resuming any cancelled subscriptions.
|
||||
axios.put(this.urlForPlanUpdate, {"plan": plan.id})
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
})
|
||||
.catch(errors => {
|
||||
if (errors.response.status == 422) {
|
||||
this.planForm.errors.set(errors.response.data);
|
||||
} else {
|
||||
this.planForm.errors.set({plan: ["We were unable to update your subscription. Please contact customer support."]});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.selectingPlan = null;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the given plan is selected.
|
||||
*/
|
||||
isActivePlan(plan) {
|
||||
return this.activeSubscription &&
|
||||
this.activeSubscription.provider_plan == plan.id;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the active plan instance.
|
||||
*/
|
||||
activePlan() {
|
||||
if (this.activeSubscription) {
|
||||
return _.find(this.plans, (plan) => {
|
||||
return plan.id == this.activeSubscription.provider_plan;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the active plan is a monthly plan.
|
||||
*/
|
||||
activePlanIsMonthly() {
|
||||
return this.activePlan && this.activePlan.interval == 'monthly';
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the active subscription instance.
|
||||
*/
|
||||
activeSubscription() {
|
||||
if ( ! this.billable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = _.find(
|
||||
this.billable.subscriptions,
|
||||
subscription => subscription.name == 'default'
|
||||
);
|
||||
|
||||
if (typeof subscription !== 'undefined') {
|
||||
return subscription;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the current subscription is active.
|
||||
*/
|
||||
subscriptionIsActive() {
|
||||
return this.activeSubscription && ! this.activeSubscription.ends_at;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the billable entity is on a generic trial.
|
||||
*/
|
||||
onGenericTrial() {
|
||||
return this.billable.trial_ends_at &&
|
||||
moment.utc(this.billable.trial_ends_at).isAfter(moment.utc());
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the current subscription is active.
|
||||
*/
|
||||
subscriptionIsOnTrial() {
|
||||
if (this.onGenericTrial) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.activeSubscription &&
|
||||
this.activeSubscription.trial_ends_at &&
|
||||
moment.utc().isBefore(moment.utc(this.activeSubscription.trial_ends_at));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the formatted trial ending date.
|
||||
*/
|
||||
trialEndsAt() {
|
||||
if ( ! this.subscriptionIsOnTrial) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.onGenericTrial) {
|
||||
return moment.utc(this.billable.trial_ends_at)
|
||||
.local().format('MMMM Do, YYYY');
|
||||
}
|
||||
|
||||
return moment.utc(this.activeSubscription.trial_ends_at)
|
||||
.local().format('MMMM Do, YYYY');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the current subscription is active.
|
||||
*/
|
||||
subscriptionIsOnGracePeriod() {
|
||||
return this.activeSubscription &&
|
||||
this.activeSubscription.ends_at &&
|
||||
moment.utc().isBefore(moment.utc(this.activeSubscription.ends_at));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the billable entity has no active subscription.
|
||||
*/
|
||||
needsSubscription() {
|
||||
return ! this.activeSubscription ||
|
||||
(this.activeSubscription.ends_at &&
|
||||
moment.utc().isAfter(moment.utc(this.activeSubscription.ends_at)));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL for the subscription plan update.
|
||||
*/
|
||||
urlForPlanUpdate() {
|
||||
return this.billingUser
|
||||
? '/settings/subscription'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/subscription`;
|
||||
}
|
||||
}
|
||||
};
|
91
spark/resources/assets/js/mixins/tab-state.js
vendored
Normal file
91
spark/resources/assets/js/mixins/tab-state.js
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
module.exports = {
|
||||
pushStateSelector: null,
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Initialize push state handling for tabs.
|
||||
*/
|
||||
usePushStateForTabs(selector) {
|
||||
this.pushStateSelector = selector;
|
||||
|
||||
this.registerTabClickHandler();
|
||||
|
||||
window.addEventListener('popstate', e => {
|
||||
this.activateTabForCurrentHash();
|
||||
});
|
||||
|
||||
if (window.location.hash) {
|
||||
this.activateTabForCurrentHash();
|
||||
} else {
|
||||
this.activateFirstTab();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Register the click handler for all of the tabs.
|
||||
*/
|
||||
registerTabClickHandler() {
|
||||
const self = this;
|
||||
|
||||
$(`${this.pushStateSelector} a[data-toggle="tab"]`).on('click', function(e) {
|
||||
self.removeActiveClassFromTabs();
|
||||
|
||||
history.pushState(null, null, '#/' + $(this).attr('href').substring(1));
|
||||
|
||||
self.broadcastTabChange($(this).attr('href').substring(1));
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Activate the tab for the current hash in the URL.
|
||||
*/
|
||||
activateTabForCurrentHash() {
|
||||
var hash = window.location.hash.substring(2);
|
||||
|
||||
var parameters = hash.split('/');
|
||||
|
||||
hash = parameters.shift();
|
||||
|
||||
this.removeActiveClassFromTabs();
|
||||
|
||||
const tab = $(`${this.pushStateSelector} a[href="#${hash}"][data-toggle="tab"]`);
|
||||
|
||||
if (tab.length > 0) {
|
||||
tab.tab('show');
|
||||
}
|
||||
|
||||
this.broadcastTabChange(hash, parameters);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Activate the first tab in a list.
|
||||
*/
|
||||
activateFirstTab() {
|
||||
const tab = $(`${this.pushStateSelector} a[data-toggle="tab"]`).first();
|
||||
|
||||
tab.tab('show');
|
||||
|
||||
this.broadcastTabChange(tab.attr('href').substring(1));
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Remove the active class from the tabs.
|
||||
*/
|
||||
removeActiveClassFromTabs() {
|
||||
$(`${this.pushStateSelector} li`).removeClass('active');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Broadcast that a tab change happened.
|
||||
*/
|
||||
broadcastTabChange(hash, parameters) {
|
||||
Bus.$emit('sparkHashChanged', hash, parameters);
|
||||
}
|
||||
}
|
||||
};
|
44
spark/resources/assets/js/mixins/vat.js
vendored
Normal file
44
spark/resources/assets/js/mixins/vat.js
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
module.exports = {
|
||||
methods: {
|
||||
/**
|
||||
* Determine if the given country collects European VAT.
|
||||
*/
|
||||
collectsVat(country) {
|
||||
return Spark.collectsEuropeanVat ? _.contains([
|
||||
'BE', 'BG', 'CZ', 'DK', 'DE',
|
||||
'EE', 'IE', 'GR', 'ES', 'FR',
|
||||
'HR', 'IT', 'CY', 'LV', 'LT',
|
||||
'LU', 'HU', 'MT', 'NL', 'AT',
|
||||
'PL', 'PT', 'RO', 'SI', 'SK',
|
||||
'FI', 'SE', 'GB',
|
||||
], country) : false;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Refresh the tax rate using the given form input.
|
||||
*/
|
||||
refreshTaxRate(form) {
|
||||
axios.post('/tax-rate', JSON.parse(JSON.stringify(form)))
|
||||
.then(response => {
|
||||
this.taxRate = response.data.rate;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the tax amount for the selected plan.
|
||||
*/
|
||||
taxAmount(plan) {
|
||||
return plan.price * (this.taxRate / 100);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the total plan price including the applicable tax.
|
||||
*/
|
||||
priceWithTax(plan) {
|
||||
return plan.price + this.taxAmount(plan);
|
||||
}
|
||||
}
|
||||
};
|
24
spark/resources/assets/js/navbar/navbar.js
vendored
Normal file
24
spark/resources/assets/js/navbar/navbar.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
props: [
|
||||
'user', 'teams', 'currentTeam',
|
||||
'hasUnreadNotifications', 'hasUnreadAnnouncements'
|
||||
],
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show the user's notifications.
|
||||
*/
|
||||
showNotifications() {
|
||||
Bus.$emit('showNotifications');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the customer support e-mail form.
|
||||
*/
|
||||
showSupportForm() {
|
||||
Bus.$emit('showSupportForm');
|
||||
}
|
||||
}
|
||||
};
|
80
spark/resources/assets/js/notifications/notifications.js
vendored
Normal file
80
spark/resources/assets/js/notifications/notifications.js
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
module.exports = {
|
||||
props: ['notifications', 'hasUnreadAnnouncements', 'loadingNotifications'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
showingNotifications: true,
|
||||
showingAnnouncements: false
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show the user notifications.
|
||||
*/
|
||||
showNotifications() {
|
||||
this.showingNotifications = true;
|
||||
this.showingAnnouncements = false;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the product announcements.
|
||||
*/
|
||||
showAnnouncements() {
|
||||
this.showingNotifications = false;
|
||||
this.showingAnnouncements = true;
|
||||
|
||||
this.updateLastReadAnnouncementsTimestamp();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the last read announcements timestamp.
|
||||
*/
|
||||
updateLastReadAnnouncementsTimestamp() {
|
||||
axios.put('/user/last-read-announcements-at')
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the active notifications or announcements.
|
||||
*/
|
||||
activeNotifications() {
|
||||
if ( ! this.notifications) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (this.showingNotifications) {
|
||||
return this.notifications.notifications;
|
||||
} else {
|
||||
return this.notifications.announcements;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has any notifications.
|
||||
*/
|
||||
hasNotifications() {
|
||||
return this.notifications && this.notifications.notifications.length > 0;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has any announcements.
|
||||
*/
|
||||
hasAnnouncements() {
|
||||
return this.notifications && this.notifications.announcements.length > 0;
|
||||
}
|
||||
}
|
||||
};
|
52
spark/resources/assets/js/settings/api.js
vendored
Normal file
52
spark/resources/assets/js/settings/api.js
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
tokens: [],
|
||||
availableAbilities: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.getTokens();
|
||||
this.getAvailableAbilities();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$on('updateTokens', function(){
|
||||
self.getTokens();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the current API tokens for the user.
|
||||
*/
|
||||
getTokens() {
|
||||
axios.get('/settings/api/tokens')
|
||||
.then(response => this.tokens = response.data);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the available token abilities.
|
||||
*/
|
||||
getAvailableAbilities() {
|
||||
axios.get('/settings/api/token/abilities')
|
||||
.then(response => this.availableAbilities = response.data);
|
||||
}
|
||||
}
|
||||
};
|
139
spark/resources/assets/js/settings/api/create-token.js
vendored
Normal file
139
spark/resources/assets/js/settings/api/create-token.js
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
module.exports = {
|
||||
props: ['availableAbilities'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
showingToken: null,
|
||||
allAbilitiesAssigned: false,
|
||||
|
||||
form: new SparkForm({
|
||||
name: '',
|
||||
abilities: []
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
copyCommandSupported() {
|
||||
return document.queryCommandSupported('copy');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the available abilities for changes.
|
||||
*/
|
||||
availableAbilities() {
|
||||
if (this.availableAbilities.length > 0) {
|
||||
this.assignDefaultAbilities();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Assign all of the default abilities.
|
||||
*/
|
||||
assignDefaultAbilities() {
|
||||
var defaults = _.filter(this.availableAbilities, a => a.default);
|
||||
|
||||
this.form.abilities = _.pluck(defaults, 'value');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Enable all the available abilities for the given token.
|
||||
*/
|
||||
assignAllAbilities() {
|
||||
this.allAbilitiesAssigned = true;
|
||||
|
||||
this.form.abilities = _.pluck(this.availableAbilities, 'value');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Remove all of the abilities from the token.
|
||||
*/
|
||||
removeAllAbilities() {
|
||||
this.allAbilitiesAssigned = false;
|
||||
|
||||
this.form.abilities = [];
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Toggle the given ability in the list of assigned abilities.
|
||||
*/
|
||||
toggleAbility(ability) {
|
||||
if (this.abilityIsAssigned(ability)) {
|
||||
this.form.abilities = _.reject(this.form.abilities, a => a == ability);
|
||||
} else {
|
||||
this.form.abilities.push(ability);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the given ability has been assigned to the token.
|
||||
*/
|
||||
abilityIsAssigned(ability) {
|
||||
return _.contains(this.form.abilities, ability);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Create a new API token.
|
||||
*/
|
||||
create() {
|
||||
Spark.post('/settings/api/token', this.form)
|
||||
.then(response => {
|
||||
this.showToken(response.token);
|
||||
|
||||
this.resetForm();
|
||||
|
||||
this.$parent.$emit('updateTokens');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Display the token to the user.
|
||||
*/
|
||||
showToken(token) {
|
||||
this.showingToken = token;
|
||||
|
||||
$('#modal-show-token').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Select the token and copy to Clipboard.
|
||||
*/
|
||||
selectToken() {
|
||||
$('#api-token').select();
|
||||
|
||||
if (this.copyCommandSupported) {
|
||||
document.execCommand("copy");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Reset the token form back to its default state.
|
||||
*/
|
||||
resetForm() {
|
||||
this.form.name = '';
|
||||
|
||||
this.assignDefaultAbilities();
|
||||
|
||||
this.allAbilitiesAssigned = false;
|
||||
}
|
||||
}
|
||||
};
|
103
spark/resources/assets/js/settings/api/tokens.js
vendored
Normal file
103
spark/resources/assets/js/settings/api/tokens.js
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
module.exports = {
|
||||
props: ['tokens', 'availableAbilities'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
updatingToken: null,
|
||||
deletingToken: null,
|
||||
|
||||
updateTokenForm: new SparkForm({
|
||||
name: '',
|
||||
abilities: []
|
||||
}),
|
||||
|
||||
deleteTokenForm: new SparkForm({})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show the edit token modal.
|
||||
*/
|
||||
editToken(token) {
|
||||
this.updatingToken = token;
|
||||
|
||||
this.initializeUpdateFormWith(token);
|
||||
|
||||
$('#modal-update-token').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Initialize the edit form with the given token.
|
||||
*/
|
||||
initializeUpdateFormWith(token) {
|
||||
this.updateTokenForm.name = token.name;
|
||||
|
||||
this.updateTokenForm.abilities = token.metadata.abilities;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the token being edited.
|
||||
*/
|
||||
updateToken() {
|
||||
Spark.put(`/settings/api/token/${this.updatingToken.id}`, this.updateTokenForm)
|
||||
.then(response => {
|
||||
this.$parent.$emit('updateTokens');
|
||||
|
||||
$('#modal-update-token').modal('hide');
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Toggle the ability on the current token being edited.
|
||||
*/
|
||||
toggleAbility(ability) {
|
||||
if (this.abilityIsAssigned(ability)) {
|
||||
this.updateTokenForm.abilities = _.reject(
|
||||
this.updateTokenForm.abilities, a => a == ability
|
||||
);
|
||||
} else {
|
||||
this.updateTokenForm.abilities.push(ability);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the ability has been assigned to the token being edited.
|
||||
*/
|
||||
abilityIsAssigned(ability) {
|
||||
return _.contains(this.updateTokenForm.abilities, ability);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get user confirmation that the token should be deleted.
|
||||
*/
|
||||
approveTokenDelete(token) {
|
||||
this.deletingToken = token;
|
||||
|
||||
$('#modal-delete-token').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Delete the specified token.
|
||||
*/
|
||||
deleteToken() {
|
||||
Spark.delete(`/settings/api/token/${this.deletingToken.id}`, this.deleteTokenForm)
|
||||
.then(() => {
|
||||
this.$parent.$emit('updateTokens');
|
||||
|
||||
$('#modal-delete-token').modal('hide');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
47
spark/resources/assets/js/settings/invoices.js
vendored
Normal file
47
spark/resources/assets/js/settings/invoices.js
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
invoices: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.getInvoices();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the user's billing invoices
|
||||
*/
|
||||
getInvoices() {
|
||||
axios.get(this.urlForInvoices)
|
||||
.then(response => {
|
||||
this.invoices = _.filter(response.data, invoice => {
|
||||
return invoice.total != '$0.00';
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for retrieving the invoices.
|
||||
*/
|
||||
urlForInvoices() {
|
||||
return this.billingUser
|
||||
? '/settings/invoices'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/invoices`;
|
||||
}
|
||||
}
|
||||
};
|
15
spark/resources/assets/js/settings/invoices/invoice-list.js
vendored
Normal file
15
spark/resources/assets/js/settings/invoices/invoice-list.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'invoices', 'billableType'],
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the URL for downloading a given invoice.
|
||||
*/
|
||||
downloadUrlFor(invoice) {
|
||||
return this.billingUser
|
||||
? `/settings/invoice/${invoice.id}`
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/invoice/${invoice.id}`;
|
||||
}
|
||||
}
|
||||
};
|
45
spark/resources/assets/js/settings/invoices/update-extra-billing-information.js
vendored
Normal file
45
spark/resources/assets/js/settings/invoices/update-extra-billing-information.js
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
information: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.information = this.billable.extra_billing_information;
|
||||
},
|
||||
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the extra billing information.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(this.urlForUpdate, this.form);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for the extra billing information method update.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return this.billingUser
|
||||
? '/settings/extra-billing-information'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/extra-billing-information`;
|
||||
}
|
||||
}
|
||||
};
|
77
spark/resources/assets/js/settings/payment-method-braintree.js
vendored
Normal file
77
spark/resources/assets/js/settings/payment-method-braintree.js
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/discounts')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The componetn's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
currentDiscount: null,
|
||||
loadingCurrentDiscount: false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$on('updateDiscount', function(){
|
||||
self.getCurrentDiscountForBillable(self.billableType, self.billable);
|
||||
|
||||
return true;
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.getCurrentDiscountForBillable(this.billableType, this.billable);
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Calculate the amount off for the given discount amount.
|
||||
*/
|
||||
calculateAmountOff(amount) {
|
||||
return amount;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the formatted discount duration for the given discount.
|
||||
*/
|
||||
formattedDiscountDuration(discount) {
|
||||
if ( ! discount) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (discount.duration) {
|
||||
case 'forever':
|
||||
return 'for all future invoices';
|
||||
case 'once':
|
||||
return 'a single invoice';
|
||||
case 'repeating':
|
||||
if (discount.duration_in_months === 1) {
|
||||
return 'all invoices during the next billing cycle';
|
||||
} else {
|
||||
return `all invoices during the next ${discount.duration_in_months} billing cycles`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
43
spark/resources/assets/js/settings/payment-method-stripe.js
vendored
Normal file
43
spark/resources/assets/js/settings/payment-method-stripe.js
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/discounts')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The componetn's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
currentDiscount: null,
|
||||
loadingCurrentDiscount: false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$on('updateDiscount', function(){
|
||||
self.getCurrentDiscountForBillable(self.billableType, self.billable);
|
||||
|
||||
return true;
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.getCurrentDiscountForBillable(this.billableType, this.billable);
|
||||
},
|
||||
};
|
41
spark/resources/assets/js/settings/payment-method/redeem-coupon.js
vendored
Normal file
41
spark/resources/assets/js/settings/payment-method/redeem-coupon.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
coupon: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Redeem the given coupon code.
|
||||
*/
|
||||
redeem() {
|
||||
Spark.post(this.urlForRedemption, this.form)
|
||||
.then(() => {
|
||||
this.form.coupon = '';
|
||||
|
||||
this.$parent.$emit('updateDiscount');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for redeeming a coupon.
|
||||
*/
|
||||
urlForRedemption() {
|
||||
return this.billingUser
|
||||
? '/settings/payment-method/coupon'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/payment-method/coupon`;
|
||||
}
|
||||
}
|
||||
};
|
104
spark/resources/assets/js/settings/payment-method/update-payment-method-braintree.js
vendored
Normal file
104
spark/resources/assets/js/settings/payment-method/update-payment-method-braintree.js
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/braintree')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
braintree_type: '',
|
||||
braintree_token: '',
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.braintree(
|
||||
'braintree-payment-method-container',
|
||||
this.braintreeCallback
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the entity's card information.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(this.urlForUpdate, this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
this.resetBraintree(
|
||||
'braintree-payment-method-container',
|
||||
this.braintreeCallback
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The Braintree payment method received callback.
|
||||
*/
|
||||
braintreeCallback(response) {
|
||||
this.form.braintree_type = response.type;
|
||||
this.form.braintree_token = response.nonce;
|
||||
|
||||
this.update();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for the payment method update.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return this.billingUser
|
||||
? '/settings/payment-method'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/payment-method`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the proper brand icon for the customer's credit card.
|
||||
*/
|
||||
cardIcon() {
|
||||
if (! this.billable.card_brand) {
|
||||
return 'fa-credit-card';
|
||||
}
|
||||
|
||||
switch (this.billable.card_brand) {
|
||||
case 'American Express':
|
||||
return 'fa-cc-amex';
|
||||
case 'Diners Club':
|
||||
return 'fa-cc-diners-club';
|
||||
case 'Discover':
|
||||
return 'fa-cc-discover';
|
||||
case 'JCB':
|
||||
return 'fa-cc-jcb';
|
||||
case 'MasterCard':
|
||||
return 'fa-cc-mastercard';
|
||||
case 'Visa':
|
||||
return 'fa-cc-visa';
|
||||
default:
|
||||
return 'fa-credit-card';
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
183
spark/resources/assets/js/settings/payment-method/update-payment-method-stripe.js
vendored
Normal file
183
spark/resources/assets/js/settings/payment-method/update-payment-method-stripe.js
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
stripe_token: '',
|
||||
address: '',
|
||||
address_line_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
country: 'US'
|
||||
}),
|
||||
|
||||
cardForm: new SparkForm({
|
||||
name: '',
|
||||
number: '',
|
||||
cvc: '',
|
||||
month: '',
|
||||
year: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
Stripe.setPublishableKey(Spark.stripeKey);
|
||||
|
||||
this.initializeBillingAddress();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Initialize the billing address form for the billable entity.
|
||||
*/
|
||||
initializeBillingAddress() {
|
||||
if (! Spark.collectsBillingAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.address = this.billable.billing_address;
|
||||
this.form.address_line_2 = this.billable.billing_address_line_2;
|
||||
this.form.city = this.billable.billing_city;
|
||||
this.form.state = this.billable.billing_state;
|
||||
this.form.zip = this.billable.billing_zip;
|
||||
this.form.country = this.billable.billing_country || 'US';
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the billable's card information.
|
||||
*/
|
||||
update() {
|
||||
this.form.busy = true;
|
||||
this.form.errors.forget();
|
||||
this.form.successful = false;
|
||||
this.cardForm.errors.forget();
|
||||
|
||||
// Here we will build out the payload to send to Stripe to obtain a card token so
|
||||
// we can create the actual subscription. We will build out this data that has
|
||||
// this credit card number, CVC, etc. and exchange it for a secure token ID.
|
||||
const payload = {
|
||||
name: this.cardForm.name,
|
||||
number: this.cardForm.number,
|
||||
cvc: this.cardForm.cvc,
|
||||
exp_month: this.cardForm.month,
|
||||
exp_year: this.cardForm.year,
|
||||
address_line1: this.form.address,
|
||||
address_line2: this.form.address_line_2,
|
||||
address_city: this.form.city,
|
||||
address_state: this.form.state,
|
||||
address_zip: this.form.zip,
|
||||
address_country: this.form.country,
|
||||
};
|
||||
|
||||
// Once we have the Stripe payload we'll send it off to Stripe and obtain a token
|
||||
// which we will send to the server to update this payment method. If there is
|
||||
// an error we will display that back out to the user for their information.
|
||||
Stripe.card.createToken(payload, (status, response) => {
|
||||
if (response.error) {
|
||||
this.cardForm.errors.set({number: [
|
||||
response.error.message
|
||||
]});
|
||||
|
||||
this.form.busy = false;
|
||||
} else {
|
||||
this.sendUpdateToServer(response.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Send the credit card update information to the server.
|
||||
*/
|
||||
sendUpdateToServer(token) {
|
||||
this.form.stripe_token = token;
|
||||
|
||||
Spark.put(this.urlForUpdate, this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
this.cardForm.name = '';
|
||||
this.cardForm.number = '';
|
||||
this.cardForm.cvc = '';
|
||||
this.cardForm.month = '';
|
||||
this.cardForm.year = '';
|
||||
|
||||
if ( ! Spark.collectsBillingAddress) {
|
||||
this.form.zip = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the billable entity's "billable" name.
|
||||
*/
|
||||
billableName() {
|
||||
return this.billingUser ? this.user.name : this.team.owner.name;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL for the payment method update.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return this.billingUser
|
||||
? '/settings/payment-method'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/payment-method`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the proper brand icon for the customer's credit card.
|
||||
*/
|
||||
cardIcon() {
|
||||
if (! this.billable.card_brand) {
|
||||
return 'fa-cc-stripe';
|
||||
}
|
||||
|
||||
switch (this.billable.card_brand) {
|
||||
case 'American Express':
|
||||
return 'fa-cc-amex';
|
||||
case 'Diners Club':
|
||||
return 'fa-cc-diners-club';
|
||||
case 'Discover':
|
||||
return 'fa-cc-discover';
|
||||
case 'JCB':
|
||||
return 'fa-cc-jcb';
|
||||
case 'MasterCard':
|
||||
return 'fa-cc-mastercard';
|
||||
case 'Visa':
|
||||
return 'fa-cc-visa';
|
||||
default:
|
||||
return 'fa-cc-stripe';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the placeholder for the billable entity's credit card.
|
||||
*/
|
||||
placeholder() {
|
||||
if (this.billable.card_last_four) {
|
||||
return `************${this.billable.card_last_four}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
};
|
42
spark/resources/assets/js/settings/payment-method/update-vat-id.js
vendored
Normal file
42
spark/resources/assets/js/settings/payment-method/update-vat-id.js
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({ vat_id: '' })
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Bootstrap the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.vat_id = this.billable.vat_id;
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the customer's VAT ID.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(this.urlForUpdate, this.form);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for the VAT ID update.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return this.billingUser
|
||||
? '/settings/payment-method/vat-id'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/payment-method/vat-id`;
|
||||
}
|
||||
}
|
||||
}
|
3
spark/resources/assets/js/settings/profile.js
vendored
Normal file
3
spark/resources/assets/js/settings/profile.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
props: ['user']
|
||||
};
|
37
spark/resources/assets/js/settings/profile/update-contact-information.js
vendored
Normal file
37
spark/resources/assets/js/settings/profile/update-contact-information.js
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: $.extend(true, new SparkForm({
|
||||
name: '',
|
||||
email: ''
|
||||
}), Spark.forms.updateContactInformation)
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Bootstrap the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.name = this.user.name;
|
||||
this.form.email = this.user.email;
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the user's contact information.
|
||||
*/
|
||||
update() {
|
||||
Spark.put('/settings/contact', this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
67
spark/resources/assets/js/settings/profile/update-profile-photo.js
vendored
Normal file
67
spark/resources/assets/js/settings/profile/update-profile-photo.js
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the user's profile photo.
|
||||
*/
|
||||
update(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if ( ! this.$refs.photo.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this.form.startProcessing();
|
||||
|
||||
// We need to gather a fresh FormData instance with the profile photo appended to
|
||||
// the data so we can POST it up to the server. This will allow us to do async
|
||||
// uploads of the profile photos. We will update the user after this action.
|
||||
axios.post('/settings/photo', this.gatherFormData())
|
||||
.then(
|
||||
() => {
|
||||
Bus.$emit('updateUser');
|
||||
|
||||
self.form.finishProcessing();
|
||||
},
|
||||
(error) => {
|
||||
self.form.setErrors(error.response.data.errors);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Gather the form data for the photo upload.
|
||||
*/
|
||||
gatherFormData() {
|
||||
const data = new FormData();
|
||||
|
||||
data.append('photo', this.$refs.photo.files[0]);
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Calculate the style attribute for the photo preview.
|
||||
*/
|
||||
previewStyle() {
|
||||
return `background-image: url(${this.user.photo_url})`;
|
||||
}
|
||||
}
|
||||
};
|
27
spark/resources/assets/js/settings/security.js
vendored
Normal file
27
spark/resources/assets/js/settings/security.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
twoFactorResetCode: null
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.$on('receivedTwoFactorResetCode', function (code) {
|
||||
self.twoFactorResetCode = code;
|
||||
|
||||
$('#modal-show-two-factor-reset-code').modal('show');
|
||||
});
|
||||
}
|
||||
};
|
25
spark/resources/assets/js/settings/security/disable-two-factor-auth.js
vendored
Normal file
25
spark/resources/assets/js/settings/security/disable-two-factor-auth.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Disable two-factor authentication for the user.
|
||||
*/
|
||||
disable() {
|
||||
Spark.delete('/settings/two-factor-auth', this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
39
spark/resources/assets/js/settings/security/enable-two-factor-auth.js
vendored
Normal file
39
spark/resources/assets/js/settings/security/enable-two-factor-auth.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
props: ['user'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
country_code: '',
|
||||
phone: ''
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.country_code = this.user.country_code;
|
||||
this.form.phone = this.user.phone;
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Enable two-factor authentication for the user.
|
||||
*/
|
||||
enable() {
|
||||
Spark.post('/settings/two-factor-auth', this.form)
|
||||
.then(code => {
|
||||
this.$parent.$emit('receivedTwoFactorResetCode', code);
|
||||
|
||||
Bus.$emit('updateUser');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
24
spark/resources/assets/js/settings/security/update-password.js
vendored
Normal file
24
spark/resources/assets/js/settings/security/update-password.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the user's password.
|
||||
*/
|
||||
update() {
|
||||
Spark.put('/settings/password', this.form);
|
||||
}
|
||||
}
|
||||
};
|
28
spark/resources/assets/js/settings/settings.js
vendored
Normal file
28
spark/resources/assets/js/settings/settings.js
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
module.exports = {
|
||||
props: ['user', 'teams'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [require('./../mixins/tab-state')],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
billableType: 'user',
|
||||
team: null
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.usePushStateForTabs('.spark-settings-tabs');
|
||||
}
|
||||
};
|
50
spark/resources/assets/js/settings/subscription.js
vendored
Normal file
50
spark/resources/assets/js/settings/subscription.js
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../mixins/plans'),
|
||||
require('./../mixins/subscriptions')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
plans: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
var self = this;
|
||||
|
||||
this.getPlans();
|
||||
|
||||
this.$on('showPlanDetails', function (plan) {
|
||||
self.showPlanDetails(plan);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the active plans for the application.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/plans')
|
||||
.then(response => {
|
||||
this.plans = this.billingUser
|
||||
? _.where(response.data, {type: "user"})
|
||||
: _.where(response.data, {type: "team"});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
48
spark/resources/assets/js/settings/subscription/cancel-subscription.js
vendored
Normal file
48
spark/resources/assets/js/settings/subscription/cancel-subscription.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Confirm the cancellation operation.
|
||||
*/
|
||||
confirmCancellation() {
|
||||
$('#modal-confirm-cancellation').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Cancel the current subscription.
|
||||
*/
|
||||
cancel() {
|
||||
Spark.delete(this.urlForCancellation, this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
$('#modal-confirm-cancellation').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for the subscription cancellation.
|
||||
*/
|
||||
urlForCancellation() {
|
||||
return this.billingUser
|
||||
? '/settings/subscription'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/subscription`;
|
||||
}
|
||||
}
|
||||
};
|
41
spark/resources/assets/js/settings/subscription/resume-subscription.js
vendored
Normal file
41
spark/resources/assets/js/settings/subscription/resume-subscription.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'plans', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/plans'),
|
||||
require('./../../mixins/subscriptions')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
if (this.onlyHasYearlyPlans) {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Show the plan details for the given plan.
|
||||
*
|
||||
* We'll ask the parent subscription component to display it.
|
||||
*/
|
||||
showPlanDetails(plan) {
|
||||
this.$parent.$emit('showPlanDetails', plan);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the plan price with the applicable VAT.
|
||||
*/
|
||||
priceWithTax(plan) {
|
||||
return plan.price + (plan.price * (this.billable.tax_rate / 100));
|
||||
}
|
||||
}
|
||||
};
|
96
spark/resources/assets/js/settings/subscription/subscribe-braintree.js
vendored
Normal file
96
spark/resources/assets/js/settings/subscription/subscribe-braintree.js
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'plans', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/braintree'),
|
||||
require('./../../mixins/plans'),
|
||||
require('./../../mixins/subscriptions')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
braintree_type: '',
|
||||
braintree_token: '',
|
||||
plan: '',
|
||||
coupon: null
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
// If only yearly subscription plans are available, we will select that interval so that we
|
||||
// can show the plans. Then we'll select the first available paid plan from the list and
|
||||
// start the form in a good default spot. The user may then select another plan later.
|
||||
if (this.onlyHasYearlyPaidPlans) {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
|
||||
// Next, we will configure the braintree container element on the page and handle the nonce
|
||||
// received callback. We'll then set the nonce and fire off the subscribe method so this
|
||||
// nonce can be used to create the subscription for the billable entity being managed.
|
||||
this.braintree('braintree-subscribe-container', response => {
|
||||
this.form.braintree_type = response.type;
|
||||
this.form.braintree_token = response.nonce;
|
||||
|
||||
this.subscribe();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Mark the given plan as selected.
|
||||
*/
|
||||
selectPlan(plan) {
|
||||
this.selectedPlan = plan;
|
||||
|
||||
this.form.plan = this.selectedPlan.id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Subscribe to the specified plan.
|
||||
*/
|
||||
subscribe() {
|
||||
Spark.post(this.urlForNewSubscription, this.form)
|
||||
.then(response => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the plan details for the given plan.
|
||||
*
|
||||
* We'll ask the parent subscription component to display it.
|
||||
*/
|
||||
showPlanDetails(plan) {
|
||||
this.$parent.$emit('showPlanDetails', plan);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for subscribing to a plan.
|
||||
*/
|
||||
urlForNewSubscription() {
|
||||
return this.billingUser
|
||||
? '/settings/subscription'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/subscription`;
|
||||
}
|
||||
}
|
||||
};
|
216
spark/resources/assets/js/settings/subscription/subscribe-stripe.js
vendored
Normal file
216
spark/resources/assets/js/settings/subscription/subscribe-stripe.js
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'plans', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/plans'),
|
||||
require('./../../mixins/subscriptions'),
|
||||
require('./../../mixins/vat')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
taxRate: 0,
|
||||
|
||||
form: new SparkForm({
|
||||
stripe_token: '',
|
||||
plan: '',
|
||||
coupon: null,
|
||||
address: '',
|
||||
address_line_2: '',
|
||||
city: '',
|
||||
state: '',
|
||||
zip: '',
|
||||
country: 'US',
|
||||
vat_id: ''
|
||||
}),
|
||||
|
||||
cardForm: new SparkForm({
|
||||
name: '',
|
||||
number: '',
|
||||
cvc: '',
|
||||
month: '',
|
||||
year: '',
|
||||
zip: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch for changes on the entire billing address.
|
||||
*/
|
||||
'currentBillingAddress': function (value) {
|
||||
if ( ! Spark.collectsEuropeanVat) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshTaxRate(this.form);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
Stripe.setPublishableKey(Spark.stripeKey);
|
||||
|
||||
this.initializeBillingAddress();
|
||||
|
||||
if (this.onlyHasYearlyPaidPlans) {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Initialize the billing address form for the billable entity.
|
||||
*/
|
||||
initializeBillingAddress() {
|
||||
this.form.address = this.billable.billing_address;
|
||||
this.form.address_line_2 = this.billable.billing_address_line_2;
|
||||
this.form.city = this.billable.billing_city;
|
||||
this.form.state = this.billable.billing_state;
|
||||
this.form.zip = this.billable.billing_zip;
|
||||
this.form.country = this.billable.billing_country || 'US';
|
||||
this.form.vat_id = this.billable.vat_id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Mark the given plan as selected.
|
||||
*/
|
||||
selectPlan(plan) {
|
||||
this.selectedPlan = plan;
|
||||
|
||||
this.form.plan = this.selectedPlan.id;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Subscribe to the specified plan.
|
||||
*/
|
||||
subscribe() {
|
||||
this.cardForm.errors.forget();
|
||||
|
||||
this.form.startProcessing();
|
||||
|
||||
// Here we will build out the payload to send to Stripe to obtain a card token so
|
||||
// we can create the actual subscription. We will build out this data that has
|
||||
// this credit card number, CVC, etc. and exchange it for a secure token ID.
|
||||
const payload = {
|
||||
name: this.cardForm.name,
|
||||
number: this.cardForm.number,
|
||||
cvc: this.cardForm.cvc,
|
||||
exp_month: this.cardForm.month,
|
||||
exp_year: this.cardForm.year,
|
||||
address_line1: this.form.address,
|
||||
address_line2: this.form.address_line_2,
|
||||
address_city: this.form.city,
|
||||
address_state: this.form.state,
|
||||
address_zip: this.form.zip,
|
||||
address_country: this.form.country
|
||||
};
|
||||
|
||||
// Next, we will send the payload to Stripe and handle the response. If we have a
|
||||
// valid token we can send that to the server and use the token to create this
|
||||
// subscription on the back-end. Otherwise, we will show the error messages.
|
||||
Stripe.card.createToken(payload, (status, response) => {
|
||||
if (response.error) {
|
||||
this.cardForm.errors.set({number: [
|
||||
response.error.message
|
||||
]})
|
||||
|
||||
this.form.busy = false;
|
||||
} else {
|
||||
this.createSubscription(response.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* After obtaining the Stripe token, create subscription on the Spark server.
|
||||
*/
|
||||
createSubscription(token) {
|
||||
this.form.stripe_token = token;
|
||||
|
||||
Spark.post(this.urlForNewSubscription, this.form)
|
||||
.then(response => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeam');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has subscribed to the given plan before.
|
||||
*/
|
||||
hasSubscribed(plan) {
|
||||
return !!_.where(this.billable.subscriptions, {provider_plan: plan.id}).length
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the plan details for the given plan.
|
||||
*
|
||||
* We'll ask the parent subscription component to display it.
|
||||
*/
|
||||
showPlanDetails(plan) {
|
||||
this.$parent.$emit('showPlanDetails', plan);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the billable entity's "billable" name.
|
||||
*/
|
||||
billableName() {
|
||||
return this.billingUser ? this.user.name : this.team.owner.name;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the selected country collects European VAT.
|
||||
*/
|
||||
countryCollectsVat() {
|
||||
return this.collectsVat(this.form.country);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL for subscribing to a plan.
|
||||
*/
|
||||
urlForNewSubscription() {
|
||||
return this.billingUser
|
||||
? '/settings/subscription'
|
||||
: `/settings/${Spark.pluralTeamString}/${this.team.id}/subscription`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the current billing address from the subscribe form.
|
||||
*
|
||||
* This used primarily for wathcing.
|
||||
*/
|
||||
currentBillingAddress() {
|
||||
return this.form.address +
|
||||
this.form.address_line_2 +
|
||||
this.form.city +
|
||||
this.form.state +
|
||||
this.form.zip +
|
||||
this.form.country +
|
||||
this.form.vat_id;
|
||||
}
|
||||
}
|
||||
};
|
92
spark/resources/assets/js/settings/subscription/update-subscription.js
vendored
Normal file
92
spark/resources/assets/js/settings/subscription/update-subscription.js
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'plans', 'billableType'],
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [
|
||||
require('./../../mixins/plans'),
|
||||
require('./../../mixins/subscriptions')
|
||||
],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
confirmingPlan: null
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.selectActivePlanInterval();
|
||||
|
||||
// We need to watch the activePlan computed property for changes so we can select
|
||||
// the proper active plan on the plan interval button group. So, we will watch
|
||||
// this property and fire off a method anytime it changes so it can sync up.
|
||||
this.$watch('activePlan', value => {
|
||||
this.selectActivePlanInterval();
|
||||
});
|
||||
|
||||
if (this.onlyHasYearlyPlans) {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Confirm the plan update with the user.
|
||||
*/
|
||||
confirmPlanUpdate(plan) {
|
||||
this.confirmingPlan = plan;
|
||||
|
||||
$('#modal-confirm-plan-update').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Approve the plan update.
|
||||
*/
|
||||
approvePlanUpdate() {
|
||||
$('#modal-confirm-plan-update').modal('hide');
|
||||
|
||||
this.updateSubscription(this.confirmingPlan);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Select the active plan interval.
|
||||
*/
|
||||
selectActivePlanInterval() {
|
||||
if (this.activePlanIsMonthly || this.yearlyPlans.length == 0) {
|
||||
this.showMonthlyPlans();
|
||||
} else {
|
||||
this.showYearlyPlans();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show the plan details for the given plan.
|
||||
*
|
||||
* We'll ask the parent subscription component to display it.
|
||||
*/
|
||||
showPlanDetails(plan) {
|
||||
this.$parent.$emit('showPlanDetails', plan);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the plan price with the applicable VAT.
|
||||
*/
|
||||
priceWithTax(plan) {
|
||||
return plan.price + (plan.price * (this.billable.tax_rate / 100));
|
||||
}
|
||||
}
|
||||
};
|
3
spark/resources/assets/js/settings/teams.js
vendored
Normal file
3
spark/resources/assets/js/settings/teams.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
props: ['user', 'teams'],
|
||||
};
|
151
spark/resources/assets/js/settings/teams/create-team.js
vendored
Normal file
151
spark/resources/assets/js/settings/teams/create-team.js
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
plans: [],
|
||||
|
||||
form: new SparkForm({
|
||||
name: '',
|
||||
slug: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the active subscription instance.
|
||||
*/
|
||||
activeSubscription() {
|
||||
if ( ! this.$parent.billable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = _.find(
|
||||
this.$parent.billable.subscriptions,
|
||||
subscription => subscription.name == 'default'
|
||||
);
|
||||
|
||||
if (typeof subscription !== 'undefined') {
|
||||
return subscription;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the active plan instance.
|
||||
*/
|
||||
activePlan() {
|
||||
if (this.activeSubscription) {
|
||||
return _.find(this.plans, (plan) => {
|
||||
return plan.id == this.activeSubscription.provider_plan;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check if there's a limit for the number of teams.
|
||||
*/
|
||||
hasTeamLimit() {
|
||||
if (! this.activePlan) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !! this.activePlan.attributes.teams;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the remaining teams in the active plan.
|
||||
*/
|
||||
remainingTeams() {
|
||||
var ownedTeams = _.filter(this.$parent.teams, {owner_id: this.$parent.billable.id});
|
||||
|
||||
return this.activePlan
|
||||
? this.activePlan.attributes.teams - ownedTeams.length
|
||||
: 0;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check if the user can create more teams.
|
||||
*/
|
||||
canCreateMoreTeams() {
|
||||
if (! this.hasTeamLimit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.remainingTeams > 0;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getPlans();
|
||||
},
|
||||
|
||||
|
||||
events: {
|
||||
/**
|
||||
* Handle the "activatedTab" event.
|
||||
*/
|
||||
activatedTab(tab) {
|
||||
if (tab === Spark.pluralTeamString) {
|
||||
Vue.nextTick(() => {
|
||||
$('#create-team-name').focus();
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
watch: {
|
||||
/**
|
||||
* Watch the team name for changes.
|
||||
*/
|
||||
'form.name': function (val, oldVal) {
|
||||
if (this.form.slug == '' ||
|
||||
this.form.slug == oldVal.toLowerCase().replace(/[\s\W-]+/g, '-')
|
||||
) {
|
||||
this.form.slug = val.toLowerCase().replace(/[\s\W-]+/g, '-');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Create a new team.
|
||||
*/
|
||||
create() {
|
||||
Spark.post('/settings/'+Spark.pluralTeamString, this.form)
|
||||
.then(() => {
|
||||
this.form.name = '';
|
||||
this.form.slug = '';
|
||||
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeams');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get all the plans defined in the application.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/plans')
|
||||
.then(response => {
|
||||
this.plans = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
85
spark/resources/assets/js/settings/teams/current-teams.js
vendored
Normal file
85
spark/resources/assets/js/settings/teams/current-teams.js
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
module.exports = {
|
||||
props: ['user', 'teams'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
leavingTeam: null,
|
||||
deletingTeam: null,
|
||||
|
||||
leaveTeamForm: new SparkForm({}),
|
||||
deleteTeamForm: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Approve leaving the given team.
|
||||
*/
|
||||
approveLeavingTeam(team) {
|
||||
this.leavingTeam = team;
|
||||
|
||||
$('#modal-leave-team').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Leave the given team.
|
||||
*/
|
||||
leaveTeam() {
|
||||
Spark.delete(this.urlForLeaving, this.leaveTeamForm)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeams');
|
||||
|
||||
$('#modal-leave-team').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Approve the deletion of the given team.
|
||||
*/
|
||||
approveTeamDelete(team) {
|
||||
this.deletingTeam = team;
|
||||
|
||||
$('#modal-delete-team').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Delete the given team.
|
||||
*/
|
||||
deleteTeam() {
|
||||
Spark.delete(`/settings/${Spark.pluralTeamString}/${this.deletingTeam.id}`, this.deleteTeamForm)
|
||||
.then(() => {
|
||||
Bus.$emit('updateUser');
|
||||
Bus.$emit('updateTeams');
|
||||
|
||||
$('#modal-delete-team').modal('hide');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for leaving a team.
|
||||
*/
|
||||
urlForLeaving() {
|
||||
return `/settings/${Spark.pluralTeamString}/${this.leavingTeam.id}/members/${this.user.id}`;
|
||||
}
|
||||
}
|
||||
};
|
16
spark/resources/assets/js/settings/teams/mailed-invitations.js
vendored
Normal file
16
spark/resources/assets/js/settings/teams/mailed-invitations.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = {
|
||||
props: ['team', 'invitations'],
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Cancel the sent invitation.
|
||||
*/
|
||||
cancel(invitation) {
|
||||
axios.delete(`/settings/invitations/${invitation.id}`)
|
||||
.then(() => {
|
||||
this.$parent.$emit('updateInvitations');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
67
spark/resources/assets/js/settings/teams/pending-invitations.js
vendored
Normal file
67
spark/resources/assets/js/settings/teams/pending-invitations.js
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
module.exports = {
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
invitations: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getPendingInvitations();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the pending invitations for the user.
|
||||
*/
|
||||
getPendingInvitations() {
|
||||
axios.get('/settings/invitations/pending')
|
||||
.then(response => {
|
||||
this.invitations = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Accept the given invitation.
|
||||
*/
|
||||
accept(invitation) {
|
||||
axios.post(`/settings/invitations/${invitation.id}/accept`)
|
||||
.then(() => {
|
||||
Bus.$emit('updateTeams');
|
||||
|
||||
this.getPendingInvitations();
|
||||
});
|
||||
|
||||
this.removeInvitation(invitation);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Reject the given invitation.
|
||||
*/
|
||||
reject(invitation) {
|
||||
axios.post(`/settings/invitations/${invitation.id}/reject`)
|
||||
.then(() => {
|
||||
this.getPendingInvitations();
|
||||
});
|
||||
|
||||
this.removeInvitation(invitation);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Remove the given invitation from the list.
|
||||
*/
|
||||
removeInvitation(invitation) {
|
||||
this.invitations = _.reject(this.invitations, i => i.id === invitation.id);
|
||||
}
|
||||
}
|
||||
};
|
116
spark/resources/assets/js/settings/teams/send-invitation.js
vendored
Normal file
116
spark/resources/assets/js/settings/teams/send-invitation.js
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
plans: [],
|
||||
|
||||
form: new SparkForm({
|
||||
email: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the active subscription instance.
|
||||
*/
|
||||
activeSubscription() {
|
||||
if ( ! this.billable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subscription = _.find(
|
||||
this.billable.subscriptions,
|
||||
subscription => subscription.name == 'default'
|
||||
);
|
||||
|
||||
if (typeof subscription !== 'undefined') {
|
||||
return subscription;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the active plan instance.
|
||||
*/
|
||||
activePlan() {
|
||||
if (this.activeSubscription) {
|
||||
return _.find(this.plans, (plan) => {
|
||||
return plan.id == this.activeSubscription.provider_plan;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check if there's a limit for the number of team members.
|
||||
*/
|
||||
hasTeamMembersLimit() {
|
||||
if (! this.activePlan) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !! this.activePlan.attributes.teamMembers;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Get the remaining team members in the active plan.
|
||||
*/
|
||||
remainingTeamMembers() {
|
||||
return this.activePlan
|
||||
? this.activePlan.attributes.teamMembers - this.$parent.team.users.length
|
||||
: 0;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check if the user can invite more team members.
|
||||
*/
|
||||
canInviteMoreTeamMembers() {
|
||||
if (! this.hasTeamMembersLimit) {
|
||||
return true;
|
||||
}
|
||||
return this.remainingTeamMembers > 0;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getPlans();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Send a team invitation.
|
||||
*/
|
||||
send() {
|
||||
Spark.post(`/settings/${Spark.pluralTeamString}/${this.team.id}/invitations`, this.form)
|
||||
.then(() => {
|
||||
this.form.email = '';
|
||||
|
||||
this.$parent.$emit('updateInvitations');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all the plans defined in the application.
|
||||
*/
|
||||
getPlans() {
|
||||
axios.get('/spark/plans')
|
||||
.then(response => {
|
||||
this.plans = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
144
spark/resources/assets/js/settings/teams/team-members.js
vendored
Normal file
144
spark/resources/assets/js/settings/teams/team-members.js
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team'],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
roles: [],
|
||||
|
||||
updatingTeamMember: null,
|
||||
deletingTeamMember: null,
|
||||
|
||||
updateTeamMemberForm: $.extend(true, new SparkForm({
|
||||
role: ''
|
||||
}), Spark.forms.updateTeamMember),
|
||||
|
||||
deleteTeamMemberForm: new SparkForm({})
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
this.getRoles();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the available team member roles.
|
||||
*/
|
||||
getRoles() {
|
||||
axios.get(`/settings/${Spark.pluralTeamString}/roles`)
|
||||
.then(response => {
|
||||
this.roles = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Edit the given team member.
|
||||
*/
|
||||
editTeamMember(member) {
|
||||
this.updatingTeamMember = member;
|
||||
this.updateTeamMemberForm.role = member.pivot.role;
|
||||
|
||||
$('#modal-update-team-member').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Update the team member.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(this.urlForUpdating, this.updateTeamMemberForm)
|
||||
.then(() => {
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
$('#modal-update-team-member').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Display the approval modal for the deletion of a team member.
|
||||
*/
|
||||
approveTeamMemberDelete(member) {
|
||||
this.deletingTeamMember = member;
|
||||
|
||||
$('#modal-delete-member').modal('show');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Delete the given team member.
|
||||
*/
|
||||
deleteMember() {
|
||||
Spark.delete(this.urlForDeleting, this.deleteTeamMemberForm)
|
||||
.then(() => {
|
||||
Bus.$emit('updateTeam');
|
||||
|
||||
$('#modal-delete-member').modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the current user can edit a team member.
|
||||
*/
|
||||
canEditTeamMember(member) {
|
||||
return this.user.id === this.team.owner_id && this.user.id !== member.id
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the current user can delete a team member.
|
||||
*/
|
||||
canDeleteTeamMember(member) {
|
||||
return this.user.id === this.team.owner_id && this.user.id !== member.id
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the displayable role for the given team member.
|
||||
*/
|
||||
teamMemberRole(member) {
|
||||
if (this.roles.length == 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (member.pivot.role == 'owner') {
|
||||
return 'Owner';
|
||||
}
|
||||
|
||||
const role = _.find(this.roles, role => role.value == member.pivot.role);
|
||||
|
||||
if (typeof role !== 'undefined') {
|
||||
return role.text;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for updating a team member.
|
||||
*/
|
||||
urlForUpdating: function () {
|
||||
return `/settings/${Spark.pluralTeamString}/${this.team.id}/members/${this.updatingTeamMember.id}`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the URL for deleting a team member.
|
||||
*/
|
||||
urlForDeleting() {
|
||||
return `/settings/${Spark.pluralTeamString}/${this.team.id}/members/${this.deletingTeamMember.id}`;
|
||||
}
|
||||
}
|
||||
};
|
39
spark/resources/assets/js/settings/teams/team-membership.js
vendored
Normal file
39
spark/resources/assets/js/settings/teams/team-membership.js
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team', 'billableType'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
invitations: []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.getInvitations();
|
||||
|
||||
this.$on('updateInvitations', function () {
|
||||
self.getInvitations();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get all of the invitations for the team.
|
||||
*/
|
||||
getInvitations() {
|
||||
axios.get(`/settings/${Spark.pluralTeamString}/${this.team.id}/invitations`)
|
||||
.then(response => {
|
||||
this.invitations = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
3
spark/resources/assets/js/settings/teams/team-profile.js
vendored
Normal file
3
spark/resources/assets/js/settings/teams/team-profile.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team']
|
||||
};
|
55
spark/resources/assets/js/settings/teams/team-settings.js
vendored
Normal file
55
spark/resources/assets/js/settings/teams/team-settings.js
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
module.exports = {
|
||||
props: ['user', 'teamId'],
|
||||
|
||||
|
||||
/**
|
||||
* Load mixins for the component.
|
||||
*/
|
||||
mixins: [require('./../../mixins/tab-state')],
|
||||
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
billableType: 'team',
|
||||
team: null
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
this.getTeam();
|
||||
|
||||
Bus.$on('updateTeam', function () {
|
||||
self.getTeam();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.usePushStateForTabs('.spark-settings-tabs');
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Get the team being managed.
|
||||
*/
|
||||
getTeam() {
|
||||
axios.get(`/${Spark.pluralTeamString}/${this.teamId}`)
|
||||
.then(response => {
|
||||
this.team = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
36
spark/resources/assets/js/settings/teams/update-team-name.js
vendored
Normal file
36
spark/resources/assets/js/settings/teams/update-team-name.js
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({
|
||||
name: ''
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the component.
|
||||
*/
|
||||
mounted() {
|
||||
this.form.name = this.team.name;
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the team name.
|
||||
*/
|
||||
update() {
|
||||
Spark.put(`/settings/${Spark.pluralTeamString}/${this.team.id}/name`, this.form)
|
||||
.then(() => {
|
||||
Bus.$emit('updateTeam');
|
||||
Bus.$emit('updateTeams');
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
76
spark/resources/assets/js/settings/teams/update-team-photo.js
vendored
Normal file
76
spark/resources/assets/js/settings/teams/update-team-photo.js
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
module.exports = {
|
||||
props: ['user', 'team'],
|
||||
|
||||
/**
|
||||
* The component's data.
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
form: new SparkForm({})
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Update the team's photo.
|
||||
*/
|
||||
update(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if ( ! this.$refs.photo.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
this.form.startProcessing();
|
||||
|
||||
// We need to gather a fresh FormData instance with the profile photo appended to
|
||||
// the data so we can POST it up to the server. This will allow us to do async
|
||||
// uploads of the profile photos. We will update the user after this action.
|
||||
axios.post(this.urlForUpdate, this.gatherFormData())
|
||||
.then(
|
||||
() => {
|
||||
Bus.$emit('updateTeam');
|
||||
Bus.$emit('updateTeams');
|
||||
|
||||
self.form.finishProcessing();
|
||||
},
|
||||
(error) => {
|
||||
self.form.setErrors(error.response.data.errors);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Gather the form data for the photo upload.
|
||||
*/
|
||||
gatherFormData() {
|
||||
const data = new FormData();
|
||||
|
||||
data.append('photo', this.$refs.photo.files[0]);
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Get the URL for updating the team photo.
|
||||
*/
|
||||
urlForUpdate() {
|
||||
return `/settings/${Spark.pluralTeamString}/${this.team.id}/photo`;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the style attribute for the photo preview.
|
||||
*/
|
||||
previewStyle() {
|
||||
return `background-image: url(${this.team.photo_url})`;
|
||||
}
|
||||
}
|
||||
};
|
80
spark/resources/assets/js/spark-bootstrap.js
vendored
Normal file
80
spark/resources/assets/js/spark-bootstrap.js
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Load various JavaScript modules that assist Spark.
|
||||
*/
|
||||
window.URI = require('urijs');
|
||||
window.axios = require('axios');
|
||||
window._ = require('underscore');
|
||||
window.moment = require('moment');
|
||||
window.Promise = require('promise');
|
||||
window.Cookies = require('js-cookie');
|
||||
|
||||
/*
|
||||
* Define Moment locales
|
||||
*/
|
||||
window.moment.defineLocale('en-short', {
|
||||
parentLocale: 'en',
|
||||
relativeTime : {
|
||||
future: "in %s",
|
||||
past: "%s",
|
||||
s: "1s",
|
||||
m: "1m",
|
||||
mm: "%dm",
|
||||
h: "1h",
|
||||
hh: "%dh",
|
||||
d: "1d",
|
||||
dd: "%dd",
|
||||
M: "1 month ago",
|
||||
MM: "%d months ago",
|
||||
y: "1y",
|
||||
yy: "%dy"
|
||||
}
|
||||
});
|
||||
window.moment.locale('en');
|
||||
|
||||
/*
|
||||
* Load jQuery and Bootstrap jQuery, used for front-end interaction.
|
||||
*/
|
||||
if (window.$ === undefined || window.jQuery === undefined) {
|
||||
window.$ = window.jQuery = require('jquery');
|
||||
}
|
||||
|
||||
require('bootstrap/dist/js/npm');
|
||||
|
||||
/**
|
||||
* Load Vue if this application is using Vue as its framework.
|
||||
*/
|
||||
if ($('#spark-app').length > 0) {
|
||||
require('vue-bootstrap');
|
||||
}
|
||||
|
||||
/**
|
||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||
* to our Laravel back-end. This library automatically handles sending the
|
||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||
*/
|
||||
window.axios.defaults.headers.common = {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRF-TOKEN': Spark.csrfToken
|
||||
};
|
||||
|
||||
/**
|
||||
* Intercept the incoming responses.
|
||||
*
|
||||
* Handle any unexpected HTTP errors and pop up modals, etc.
|
||||
*/
|
||||
window.axios.interceptors.response.use(function (response) {
|
||||
return response;
|
||||
}, function (error) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
window.axios.get('/logout');
|
||||
$('#modal-session-expired').modal('show');
|
||||
break;
|
||||
|
||||
case 402:
|
||||
window.location = '/settings#/subscription';
|
||||
break;
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
266
spark/resources/assets/js/spark.js
vendored
Normal file
266
spark/resources/assets/js/spark.js
vendored
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Export the root Spark application.
|
||||
*/
|
||||
module.exports = {
|
||||
el: '#spark-app',
|
||||
|
||||
|
||||
/**
|
||||
* Holds the timestamp for the last time we updated the API token.
|
||||
*/
|
||||
lastRefreshedApiTokenAt: null,
|
||||
|
||||
|
||||
/**
|
||||
* The application's data.
|
||||
*/
|
||||
data: {
|
||||
user: Spark.state.user,
|
||||
teams: Spark.state.teams,
|
||||
currentTeam: Spark.state.currentTeam,
|
||||
|
||||
loadingNotifications: false,
|
||||
notifications: null,
|
||||
|
||||
supportForm: new SparkForm({
|
||||
from: '',
|
||||
subject: '',
|
||||
message: ''
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* The component has been created by Vue.
|
||||
*/
|
||||
created() {
|
||||
var self = this;
|
||||
|
||||
if (Spark.userId) {
|
||||
this.loadDataForAuthenticatedUser();
|
||||
}
|
||||
|
||||
if (Spark.userId && Spark.usesApi) {
|
||||
this.refreshApiTokenEveryFewMinutes();
|
||||
}
|
||||
|
||||
Bus.$on('updateUser', function () {
|
||||
self.getUser();
|
||||
});
|
||||
|
||||
Bus.$on('updateUserData', function() {
|
||||
self.loadDataForAuthenticatedUser();
|
||||
});
|
||||
|
||||
Bus.$on('updateTeams', function () {
|
||||
self.getTeams();
|
||||
});
|
||||
|
||||
Bus.$on('showNotifications', function () {
|
||||
$('#modal-notifications').modal('show');
|
||||
|
||||
self.markNotificationsAsRead();
|
||||
});
|
||||
|
||||
Bus.$on('showSupportForm', function () {
|
||||
if (self.user) {
|
||||
self.supportForm.from = self.user.email;
|
||||
}
|
||||
|
||||
$('#modal-support').modal('show');
|
||||
|
||||
setTimeout(() => {
|
||||
$('#support-subject').focus();
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Prepare the application.
|
||||
*/
|
||||
mounted() {
|
||||
this.whenReady();
|
||||
},
|
||||
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Finish bootstrapping the application.
|
||||
*/
|
||||
whenReady() {
|
||||
//
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Load the data for an authenticated user.
|
||||
*/
|
||||
loadDataForAuthenticatedUser() {
|
||||
this.getNotifications();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Refresh the current API token every few minutes.
|
||||
*/
|
||||
refreshApiTokenEveryFewMinutes() {
|
||||
this.lastRefreshedApiTokenAt = moment();
|
||||
|
||||
setInterval(() => {
|
||||
this.refreshApiToken();
|
||||
}, 240000);
|
||||
|
||||
setInterval(() => {
|
||||
if (moment().diff(this.lastRefreshedApiTokenAt, 'minutes') >= 5) {
|
||||
this.refreshApiToken();
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Refresh the current API token.
|
||||
*/
|
||||
refreshApiToken() {
|
||||
this.lastRefreshedApiTokenAt = moment();
|
||||
|
||||
axios.put('/spark/token');
|
||||
},
|
||||
|
||||
|
||||
/*
|
||||
* Get the current user of the application.
|
||||
*/
|
||||
getUser() {
|
||||
axios.get('/user/current')
|
||||
.then(response => {
|
||||
this.user = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the current team list.
|
||||
*/
|
||||
getTeams() {
|
||||
axios.get('/'+Spark.pluralTeamString)
|
||||
.then(response => {
|
||||
this.teams = response.data;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the current team.
|
||||
*/
|
||||
getCurrentTeam() {
|
||||
axios.get(`/${Spark.pluralTeamString}/current`)
|
||||
.then(response => {
|
||||
this.currentTeam = response.data;
|
||||
})
|
||||
.catch(response => {
|
||||
//
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Get the application notifications.
|
||||
*/
|
||||
getNotifications() {
|
||||
this.loadingNotifications = true;
|
||||
|
||||
axios.get('/notifications/recent')
|
||||
.then(response => {
|
||||
this.notifications = response.data;
|
||||
|
||||
this.loadingNotifications = false;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Mark the current notifications as read.
|
||||
*/
|
||||
markNotificationsAsRead() {
|
||||
if ( ! this.hasUnreadNotifications) {
|
||||
return;
|
||||
}
|
||||
|
||||
axios.put('/notifications/read', {
|
||||
notifications: _.pluck(this.notifications.notifications, 'id')
|
||||
});
|
||||
|
||||
_.each(this.notifications.notifications, notification => {
|
||||
notification.read = 1;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Send a customer support request.
|
||||
*/
|
||||
sendSupportRequest() {
|
||||
Spark.post('/support/email', this.supportForm)
|
||||
.then(() => {
|
||||
$('#modal-support').modal('hide');
|
||||
|
||||
this.showSupportRequestSuccessMessage();
|
||||
|
||||
this.supportForm.subject = '';
|
||||
this.supportForm.message = '';
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Show an alert informing the user their support request was sent.
|
||||
*/
|
||||
showSupportRequestSuccessMessage() {
|
||||
swal({
|
||||
title: 'Got It!',
|
||||
text: 'We have received your message and will respond soon!',
|
||||
type: 'success',
|
||||
showConfirmButton: false,
|
||||
timer: 2000
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Determine if the user has any unread notifications.
|
||||
*/
|
||||
hasUnreadAnnouncements() {
|
||||
if (this.notifications && this.user) {
|
||||
if (this.notifications.announcements.length && ! this.user.last_read_announcements_at) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _.filter(this.notifications.announcements, announcement => {
|
||||
return moment.utc(this.user.last_read_announcements_at).isBefore(
|
||||
moment.utc(announcement.created_at)
|
||||
);
|
||||
}).length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the user has any unread notifications.
|
||||
*/
|
||||
hasUnreadNotifications() {
|
||||
if (this.notifications) {
|
||||
return _.filter(this.notifications.notifications, notification => {
|
||||
return ! notification.read;
|
||||
}).length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
25
spark/resources/assets/js/vue-bootstrap.js
vendored
Normal file
25
spark/resources/assets/js/vue-bootstrap.js
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Load Vue & Vue-Resource.
|
||||
*
|
||||
* Vue is the JavaScript framework used by Spark.
|
||||
*/
|
||||
if (window.Vue === undefined) {
|
||||
window.Vue = require('vue');
|
||||
|
||||
window.Bus = new Vue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load Vue Global Mixin.
|
||||
*/
|
||||
Vue.mixin(require('./mixin'));
|
||||
|
||||
/**
|
||||
* Define the Vue filters.
|
||||
*/
|
||||
require('./filters');
|
||||
|
||||
/**
|
||||
* Load the Spark form utilities.
|
||||
*/
|
||||
require('./forms/bootstrap');
|
125
spark/resources/assets/less/components/notifications.less
Normal file
125
spark/resources/assets/less/components/notifications.less
Normal file
@@ -0,0 +1,125 @@
|
||||
#modal-notifications {
|
||||
.modal-header {
|
||||
background: #fff;
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
height: 70px;
|
||||
width: 350px;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
|
||||
.btn-group {
|
||||
padding-top: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #f5f8fa;
|
||||
border: none;
|
||||
overflow-y: scroll;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background: #fff;
|
||||
border-top: 1px dashed rgba(0,0,0,.1);
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.notification-container:first-child {
|
||||
padding-top: 70px;
|
||||
}
|
||||
|
||||
.notification-container:last-child {
|
||||
padding-bottom: 65px + 23px;
|
||||
}
|
||||
|
||||
.notification:not(:first-child) {
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.notification:not(:last-child) {
|
||||
margin-bottom: 23px;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: relative;
|
||||
padding: 20px 15px 0px 15px;
|
||||
border-top: 1px dashed rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
figure { position: absolute; }
|
||||
|
||||
.notification-content {
|
||||
padding-left: 70px;
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
align-content: flex-end;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
margin: 2px 0 10px;
|
||||
}
|
||||
|
||||
.date { color: #aaa; }
|
||||
|
||||
.notification-body {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.spark-profile-photo {
|
||||
height: 52px;
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
|
||||
.fa-stack {
|
||||
color: @spark-border-color;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 15px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.modal.docked .modal-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.modal.docked.docked-left .modal-dialog { left: 0; }
|
||||
.modal.docked.docked-right .modal-dialog { right: 0; }
|
||||
|
||||
.modal.docked .modal-content {
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.modal.docked .modal-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
7
spark/resources/assets/less/components/plans.less
Normal file
7
spark/resources/assets/less/components/plans.less
Normal file
@@ -0,0 +1,7 @@
|
||||
.btn-plan {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.plan-feature-list li {
|
||||
line-height: 35px;
|
||||
}
|
27
spark/resources/assets/less/components/settings.less
Normal file
27
spark/resources/assets/less/components/settings.less
Normal file
@@ -0,0 +1,27 @@
|
||||
.spark-settings-stacked-tabs {
|
||||
border-radius: @border-radius-base;
|
||||
font-weight: 300;
|
||||
|
||||
a {
|
||||
border-bottom: 1px solid lighten(@spark-border-color, 5%);
|
||||
border-left: 3px solid transparent;
|
||||
color: @text-color;
|
||||
|
||||
i {
|
||||
color: lighten(@text-color, 25%);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
li:last-child a {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
li.active a {
|
||||
border-left: 3px solid @brand-primary;
|
||||
}
|
||||
|
||||
li a:active, li a:hover, li a:link, li a:visited {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
37
spark/resources/assets/less/components/terms.less
Normal file
37
spark/resources/assets/less/components/terms.less
Normal file
@@ -0,0 +1,37 @@
|
||||
.terms-of-service {
|
||||
h1 {
|
||||
font-size: 26px;
|
||||
margin-top: 35px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 25px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: 25px;
|
||||
}
|
||||
}
|
3
spark/resources/assets/less/elements/alerts.less
Normal file
3
spark/resources/assets/less/elements/alerts.less
Normal file
@@ -0,0 +1,3 @@
|
||||
.alert {
|
||||
font-weight: 300;
|
||||
}
|
97
spark/resources/assets/less/elements/buttons.less
Normal file
97
spark/resources/assets/less/elements/buttons.less
Normal file
@@ -0,0 +1,97 @@
|
||||
// Basic Buttons
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-info, .btn-success, .btn-warning, .btn-danger {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.btn-default:hover {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
// Outline Buttons
|
||||
.button-outline-variant(@color; @activeTextColor: #fff) {
|
||||
color: @color;
|
||||
background-color: transparent;
|
||||
border-color: @color;
|
||||
|
||||
&:focus,
|
||||
&.focus,
|
||||
&:hover,
|
||||
&:active,
|
||||
&.active,
|
||||
.open > .dropdown-toggle& {
|
||||
color: @activeTextColor;
|
||||
background-color: @color;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&.disabled,
|
||||
&[disabled],
|
||||
fieldset[disabled] & {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.focus,
|
||||
&:active,
|
||||
&.active {
|
||||
border-color: @color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-default-outline {
|
||||
.button-outline-variant(@btn-default-color);
|
||||
}
|
||||
|
||||
.btn-primary-outline {
|
||||
.button-outline-variant(@btn-primary-border);
|
||||
}
|
||||
|
||||
.btn-success-outline {
|
||||
.button-outline-variant(@btn-success-border);
|
||||
}
|
||||
|
||||
.btn-info-outline {
|
||||
.button-outline-variant(@btn-info-border);
|
||||
}
|
||||
|
||||
.btn-warning-outline {
|
||||
.button-outline-variant(@btn-warning-border);
|
||||
}
|
||||
|
||||
.btn-danger-outline {
|
||||
.button-outline-variant(@btn-danger-border);
|
||||
}
|
||||
|
||||
// File Upload Button
|
||||
.btn-upload {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
input[type="file"] {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Other Button Utilities
|
||||
.btn-table-align {
|
||||
padding-top: @padding-base-vertical + 1px;
|
||||
padding-bottom: @padding-base-vertical + 1px;
|
||||
}
|
||||
|
||||
.fa-btn {
|
||||
.m-r-xs;
|
||||
}
|
19
spark/resources/assets/less/elements/forms.less
Normal file
19
spark/resources/assets/less/elements/forms.less
Normal file
@@ -0,0 +1,19 @@
|
||||
.spark-screen {
|
||||
form h2 {
|
||||
background-color: @panel-default-heading-bg;
|
||||
border-radius: @border-radius-base;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.radio label, .checkbox label {
|
||||
font-weight: 300;
|
||||
}
|
51
spark/resources/assets/less/elements/images.less
Normal file
51
spark/resources/assets/less/elements/images.less
Normal file
@@ -0,0 +1,51 @@
|
||||
.spark-profile-photo {
|
||||
border: 2px solid @spark-border-color;
|
||||
border-radius: 50%;
|
||||
height: 40px;
|
||||
padding: 2px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.spark-profile-photo-lg {
|
||||
.spark-profile-photo;
|
||||
height: 75px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.spark-profile-photo-xl {
|
||||
.spark-profile-photo;
|
||||
height: 125px;
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
.spark-nav-profile-photo {
|
||||
.spark-profile-photo;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.spark-team-photo {
|
||||
.spark-profile-photo;
|
||||
}
|
||||
|
||||
.spark-team-photo-xs {
|
||||
border-radius: 50%;
|
||||
height: 1.28571429em;
|
||||
width: 1.28571429em;
|
||||
}
|
||||
|
||||
.spark-screen {
|
||||
.profile-photo-preview {
|
||||
.img-rounded;
|
||||
display: inline-block;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
height: 150px;
|
||||
vertical-align: middle;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.team-photo-preview {
|
||||
.profile-photo-preview;
|
||||
}
|
||||
}
|
71
spark/resources/assets/less/elements/navbar.less
Normal file
71
spark/resources/assets/less/elements/navbar.less
Normal file
@@ -0,0 +1,71 @@
|
||||
.navbar-inverse .navbar-brand {
|
||||
color: @navbar-inverse-link-color;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.navbar-inverse .navbar-brand:hover,
|
||||
.navbar-inverse .navbar-brand:focus {
|
||||
color: @navbar-inverse-link-color;
|
||||
}
|
||||
|
||||
.navbar-nav > li > a {
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.navbar-nav > li > a,
|
||||
.navbar-brand,
|
||||
.hamburger {
|
||||
height: 70px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-icon {
|
||||
padding-top: 3px;
|
||||
position: relative;
|
||||
|
||||
.activity-indicator {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 10px;
|
||||
background: @brand-danger;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
transition: all .15s;
|
||||
transform: scale(.85);
|
||||
}
|
||||
|
||||
.icon { font-size: 21px; }
|
||||
}
|
||||
|
||||
.has-activity-indicator:hover .activity-indicator {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.navbar .dropdown-header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.navbar .dropdown-menu > li > a {
|
||||
font-weight: 300;
|
||||
|
||||
i {
|
||||
color: lighten(@text-color, 25%);
|
||||
}
|
||||
|
||||
i.text-success {
|
||||
color: @brand-success;
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger { float: right; }
|
||||
|
||||
.with-navbar {
|
||||
padding-top: 100px;
|
||||
}
|
18
spark/resources/assets/less/elements/panels.less
Normal file
18
spark/resources/assets/less/elements/panels.less
Normal file
@@ -0,0 +1,18 @@
|
||||
.panel {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.panel-flush {
|
||||
.panel-body, .panel-header {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
9
spark/resources/assets/less/elements/tables.less
Normal file
9
spark/resources/assets/less/elements/tables.less
Normal file
@@ -0,0 +1,9 @@
|
||||
.table-borderless {
|
||||
> thead > tr > th {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
> tbody > tr > td {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
47
spark/resources/assets/less/spacing.less
Normal file
47
spark/resources/assets/less/spacing.less
Normal file
@@ -0,0 +1,47 @@
|
||||
.p-t-lg { padding-top: 20px; }
|
||||
.p-r-lg { padding-right: 20px; }
|
||||
.p-b-lg { padding-bottom: 20px; }
|
||||
.p-l-lg { padding-left: 20px; }
|
||||
|
||||
.p-md { padding: 15px; }
|
||||
.p-t-md { padding-top: 15px; }
|
||||
.p-r-md { padding-right: 15px; }
|
||||
.p-b-md { padding-bottom: 15px; }
|
||||
.p-l-md { padding-left: 15px; }
|
||||
|
||||
.p-t-xs { padding-top: 5px; }
|
||||
.p-r-xs { padding-right: 5px; }
|
||||
.p-b-xs { padding-bottom: 5px; }
|
||||
.p-l-xs { padding-left: 5px; }
|
||||
|
||||
.p-none { padding: 0; }
|
||||
.p-t-none { padding-top: 0; }
|
||||
.p-r-none { padding-right: 0; }
|
||||
.p-b-none { padding-bottom: 0; }
|
||||
.p-l-none { padding-left: 0; }
|
||||
|
||||
.m-t-lg { margin-top: 20px; }
|
||||
.m-r-lg { margin-right: 20px; }
|
||||
.m-b-lg { margin-bottom: 20px; }
|
||||
.m-l-lg { margin-left: 20px; }
|
||||
|
||||
.m-t-md { margin-top: 15px; }
|
||||
.m-r-md { margin-right: 15px; }
|
||||
.m-b-md { margin-bottom: 15px; }
|
||||
.m-l-md { margin-left: 15px; }
|
||||
|
||||
.m-t-sm { margin-top: 10px; }
|
||||
.m-r-sm { margin-right: 10px; }
|
||||
.m-b-sm { margin-bottom: 10px; }
|
||||
.m-l-sm { margin-left: 10px; }
|
||||
|
||||
.m-t-xs { margin-top: 5px; }
|
||||
.m-r-xs { margin-right: 5px; }
|
||||
.m-b-xs { margin-bottom: 5px; }
|
||||
.m-l-xs { margin-left: 5px; }
|
||||
|
||||
.m-none { margin: 0; }
|
||||
.m-t-none { margin-top: 0; }
|
||||
.m-r-none { margin-right: 0; }
|
||||
.m-b-none { margin-bottom: 0; }
|
||||
.m-l-none { margin-left: 0; }
|
33
spark/resources/assets/less/spark.less
Normal file
33
spark/resources/assets/less/spark.less
Normal file
@@ -0,0 +1,33 @@
|
||||
// Import Variables
|
||||
@import "variables";
|
||||
|
||||
// Spacing Helpers
|
||||
@import "spacing";
|
||||
|
||||
// Utility Helpers
|
||||
@import "utilities";
|
||||
|
||||
// HTML Elements
|
||||
@import "elements/alerts";
|
||||
@import "elements/buttons";
|
||||
@import "elements/forms";
|
||||
@import "elements/images";
|
||||
@import "elements/navbar";
|
||||
@import "elements/panels";
|
||||
@import "elements/tables";
|
||||
|
||||
// Component Styling
|
||||
@import "components/notifications";
|
||||
@import "components/plans";
|
||||
@import "components/settings";
|
||||
@import "components/terms";
|
||||
|
||||
// Body Styling
|
||||
body {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
// Vue Cloak
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
3
spark/resources/assets/less/utilities.less
Normal file
3
spark/resources/assets/less/utilities.less
Normal file
@@ -0,0 +1,3 @@
|
||||
.border-none {
|
||||
border: 0;
|
||||
}
|
59
spark/resources/assets/less/variables.less
Normal file
59
spark/resources/assets/less/variables.less
Normal file
@@ -0,0 +1,59 @@
|
||||
// Body
|
||||
@body-bg: #f5f8fa;
|
||||
|
||||
// Base Border Color
|
||||
@spark-border-color: darken(@body-bg, 10%);
|
||||
|
||||
// Set Common Borders
|
||||
@list-group-border: @spark-border-color;
|
||||
@navbar-default-border: @spark-border-color;
|
||||
@panel-default-border: @spark-border-color;
|
||||
@panel-inner-border: @spark-border-color;
|
||||
|
||||
// Brands
|
||||
@brand-primary: #3097D1;
|
||||
@brand-info: #8eb4cb;
|
||||
@brand-success: #4eb76e;
|
||||
@brand-warning: #cbb956;
|
||||
@brand-danger: #bf5329;
|
||||
|
||||
// Typography
|
||||
@font-family-sans-serif: "Open Sans", Helvetica, Arial, sans-serif;
|
||||
@line-height-base: 1.6;
|
||||
@text-color: #636b6f;
|
||||
|
||||
// Buttons
|
||||
@btn-default-color: @text-color;
|
||||
@btn-font-size: @font-size-base;
|
||||
@btn-font-weight: 300;
|
||||
|
||||
// Inputs
|
||||
@input-border: lighten(@text-color, 40%);
|
||||
@input-border-focus: lighten(@brand-primary, 25%);
|
||||
@input-color-placeholder: lighten(@text-color, 30%);
|
||||
|
||||
// Navbar
|
||||
@navbar-height: 50px;
|
||||
@navbar-margin-bottom: 0;
|
||||
|
||||
@navbar-inverse-bg: #fff;
|
||||
@navbar-inverse-color: lighten(@text-color, 30%);
|
||||
@navbar-inverse-border: @spark-border-color;
|
||||
|
||||
@navbar-inverse-link-color: lighten(@text-color, 25%);
|
||||
@navbar-inverse-link-active-bg: transparent;
|
||||
@navbar-inverse-link-active-color: @navbar-inverse-link-hover-color;
|
||||
@navbar-inverse-link-hover-color: darken(@navbar-inverse-link-color, 5%);
|
||||
|
||||
@navbar-inverse-toggle-border-color: @spark-border-color;
|
||||
@navbar-inverse-toggle-hover-bg: @navbar-inverse-bg;
|
||||
@navbar-inverse-toggle-icon-bar-bg: @spark-border-color;
|
||||
|
||||
// Dropdowns
|
||||
@dropdown-anchor-padding: 5px 20px;
|
||||
@dropdown-border: @spark-border-color;
|
||||
@dropdown-divider-bg: lighten(@spark-border-color, 5%);
|
||||
@dropdown-header-color: darken(@text-color, 10%);
|
||||
@dropdown-link-color: @text-color;
|
||||
@dropdown-link-hover-bg: #fff;
|
||||
@dropdown-padding: 10px 0;
|
5
spark/resources/lang/en/validation.php
Normal file
5
spark/resources/lang/en/validation.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'eligibility' => 'You are not eligible for this plan based on your current number of '.str_plural(Spark::teamString()).' / '.Spark::teamString().' members.'
|
||||
];
|
1
spark/resources/views/auth/emails/password.blade.php
Normal file
1
spark/resources/views/auth/emails/password.blade.php
Normal file
@@ -0,0 +1 @@
|
||||
Click here to reset your password: <a href="{{ $link = url('password/reset', $token).'?email='.urlencode($user->getEmailForPasswordReset()) }}"> {{ $link }} </a>
|
@@ -0,0 +1,46 @@
|
||||
@extends('spark::layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Login Via Emergency Token</div>
|
||||
|
||||
<div class="panel-body">
|
||||
@include('spark::shared.errors')
|
||||
|
||||
<!-- Warning Message -->
|
||||
<div class="alert alert-warning">
|
||||
After logging in via your emergency token, two-factor authentication will be
|
||||
disabled for your account. If you would like to maintain two-factor
|
||||
authentication security, you should re-enable it after logging in.
|
||||
</div>
|
||||
|
||||
<form class="form-horizontal" role="form" method="POST" action="/login-via-emergency-token">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<!-- Emergency Token -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label">Emergency Token</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="password" class="form-control" name="token" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emergency Token Login Button -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-8 col-md-offset-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-btn fa-sign-in"></i>Login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
61
spark/resources/views/auth/login.blade.php
Normal file
61
spark/resources/views/auth/login.blade.php
Normal file
@@ -0,0 +1,61 @@
|
||||
@extends('spark::layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Login</div>
|
||||
|
||||
<div class="panel-body">
|
||||
@include('spark::shared.errors')
|
||||
|
||||
<form class="form-horizontal" role="form" method="POST" action="/login">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<!-- E-Mail Address -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label">E-Mail Address</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="email" class="form-control" name="email" value="{{ old('email') }}" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label">Password</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="password" class="form-control" name="password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-6 col-md-offset-4">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="remember"> Remember Me
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Button -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-8 col-md-offset-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa m-r-xs fa-sign-in"></i>Login
|
||||
</button>
|
||||
|
||||
<a class="btn btn-link" href="{{ url('/password/reset') }}">Forgot Your Password?</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
50
spark/resources/views/auth/passwords/email.blade.php
Normal file
50
spark/resources/views/auth/passwords/email.blade.php
Normal file
@@ -0,0 +1,50 @@
|
||||
@extends('spark::layouts.app')
|
||||
|
||||
<!-- Main Content -->
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Reset Password</div>
|
||||
|
||||
<div class="panel-body">
|
||||
@if (session('status'))
|
||||
<div class="alert alert-success">
|
||||
{{ session('status') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form class="form-horizontal" role="form" method="POST" action="{{ url('/password/email') }}">
|
||||
{!! csrf_field() !!}
|
||||
|
||||
<!-- E-Mail Address -->
|
||||
<div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
|
||||
<label class="col-md-4 control-label">E-Mail Address</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="email" class="form-control" name="email" value="{{ old('email') }}" autofocus>
|
||||
|
||||
@if ($errors->has('email'))
|
||||
<span class="help-block">
|
||||
{{ $errors->first('email') }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Send Password Reset Link Button -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-6 col-md-offset-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-btn fa-envelope"></i>Send Password Reset Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
74
spark/resources/views/auth/passwords/reset.blade.php
Normal file
74
spark/resources/views/auth/passwords/reset.blade.php
Normal file
@@ -0,0 +1,74 @@
|
||||
@extends('spark::layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Reset Password</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" role="form" method="POST" action="{{ url('/password/reset') }}">
|
||||
{!! csrf_field() !!}
|
||||
|
||||
<input type="hidden" name="token" value="{{ $token }}">
|
||||
|
||||
<!-- E-Mail Address -->
|
||||
<div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
|
||||
<label class="col-md-4 control-label">E-Mail Address</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="email" class="form-control" name="email" value="{{ $email or old('email') }}" autofocus>
|
||||
|
||||
@if ($errors->has('email'))
|
||||
<span class="help-block">
|
||||
{{ $errors->first('email') }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}">
|
||||
<label class="col-md-4 control-label">Password</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="password" class="form-control" name="password">
|
||||
|
||||
@if ($errors->has('password'))
|
||||
<span class="help-block">
|
||||
{{ $errors->first('password') }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Confirmation -->
|
||||
<div class="form-group{{ $errors->has('password_confirmation') ? ' has-error' : '' }}">
|
||||
<label class="col-md-4 control-label">Confirm Password</label>
|
||||
<div class="col-md-6">
|
||||
<input type="password" class="form-control" name="password_confirmation">
|
||||
|
||||
@if ($errors->has('password_confirmation'))
|
||||
<span class="help-block">
|
||||
{{ $errors->first('password_confirmation') }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reset Button -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-6 col-md-offset-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-btn fa-refresh"></i>Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
91
spark/resources/views/auth/register-address.blade.php
Normal file
91
spark/resources/views/auth/register-address.blade.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<!-- Address -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('address')}">
|
||||
<label class="col-md-4 control-label">Address</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" v-model="registerForm.address" lazy>
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('address')">
|
||||
@{{ registerForm.errors.get('address') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Address Line 2 -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('address_line_2')}">
|
||||
<label class="col-md-4 control-label">Address Line 2</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" v-model="registerForm.address_line_2" lazy>
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('address_line_2')">
|
||||
@{{ registerForm.errors.get('address_line_2') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- City -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('city')}">
|
||||
<label class="col-md-4 control-label">City</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" v-model.lazy="registerForm.city">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('city')">
|
||||
@{{ registerForm.errors.get('city') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- State & ZIP Code -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('state')}">
|
||||
<label class="col-md-4 control-label">State & ZIP / Postal Code</label>
|
||||
|
||||
<!-- State -->
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" placeholder="State" v-model.lazy="registerForm.state">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('state')">
|
||||
@{{ registerForm.errors.get('state') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Zip Code -->
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" placeholder="Postal Code" v-model.lazy="registerForm.zip">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('zip')">
|
||||
@{{ registerForm.errors.get('zip') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Country -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('country')}">
|
||||
<label class="col-md-4 control-label">Country</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<select class="form-control" v-model.lazy="registerForm.country">
|
||||
@foreach (app(Laravel\Spark\Repositories\Geography\CountryRepository::class)->all() as $key => $country)
|
||||
<option value="{{ $key }}">{{ $country }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('country')">
|
||||
@{{ registerForm.errors.get('country') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- European VAT ID -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('vat_id')}" v-if="countryCollectsVat">
|
||||
<label class="col-md-4 control-label">VAT ID</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" v-model.lazy="registerForm.vat_id">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('vat_id')">
|
||||
@{{ registerForm.errors.get('vat_id') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
85
spark/resources/views/auth/register-braintree.blade.php
Normal file
85
spark/resources/views/auth/register-braintree.blade.php
Normal file
@@ -0,0 +1,85 @@
|
||||
@extends('spark::layouts.app')
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://js.braintreegateway.com/v2/braintree.js"></script>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<spark-register-braintree inline-template>
|
||||
<div>
|
||||
<div class="spark-screen container">
|
||||
<!-- Common Register Form Contents -->
|
||||
@include('spark::auth.register-common')
|
||||
|
||||
<!-- Billing Information -->
|
||||
<div class="row" v-show="selectedPlan && selectedPlan.price > 0">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><i class="fa fa-btn fa-credit-card"></i>Billing</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<!-- Generic 500 Level Error Message / Stripe Threw Exception -->
|
||||
<div class="alert alert-danger" v-if="registerForm.errors.has('form')">
|
||||
We had trouble validating your card. It's possible your card provider is preventing
|
||||
us from charging the card. Please contact your card provider or customer support.
|
||||
</div>
|
||||
|
||||
<form class="form-horizontal" role="form">
|
||||
<!-- Braintree Container -->
|
||||
<div id="braintree-container" class="m-b-sm"></div>
|
||||
|
||||
<!-- Coupon Code -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('coupon')}" v-if="query.coupon">
|
||||
<label for="number" class="col-md-4 control-label">Coupon Code</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" name="coupon" v-model="registerForm.coupon">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('coupon')">
|
||||
@{{ registerForm.errors.get('coupon') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms And Conditions -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('terms')}">
|
||||
<div class="col-sm-6 col-sm-offset-4">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" v-model="registerForm.terms">
|
||||
I Accept The <a href="/terms" target="_blank">Terms Of Service</a>
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('terms')">
|
||||
<strong>@{{ registerForm.errors.get('terms') }}</strong>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Button -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-6 col-sm-offset-4">
|
||||
<button type="submit" class="btn btn-primary" :disabled="registerForm.busy">
|
||||
<span v-if="registerForm.busy">
|
||||
<i class="fa fa-btn fa-spinner fa-spin"></i>Registering
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<i class="fa fa-btn fa-check-circle"></i>Register
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plan Features Modal -->
|
||||
@include('spark::modals.plan-details')
|
||||
</div>
|
||||
</spark-register-braintree>
|
||||
@endsection
|
119
spark/resources/views/auth/register-common-form.blade.php
Normal file
119
spark/resources/views/auth/register-common-form.blade.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<form class="form-horizontal" role="form">
|
||||
@if (Spark::usesTeams() && Spark::onlyTeamPlans())
|
||||
<!-- Team Name -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('team')}" v-if=" ! invitation">
|
||||
<label class="col-md-4 control-label">{{ ucfirst(Spark::teamString()) }} Name</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="team" v-model="registerForm.team" autofocus>
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('team')">
|
||||
@{{ registerForm.errors.get('team') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Spark::teamsIdentifiedByPath())
|
||||
<!-- Team Slug (Only Shown When Using Paths For Teams) -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('team_slug')}" v-if=" ! invitation">
|
||||
<label class="col-md-4 control-label">{{ ucfirst(Spark::teamString()) }} Slug</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="team_slug" v-model="registerForm.team_slug" autofocus>
|
||||
|
||||
<p class="help-block" v-show=" ! registerForm.errors.has('team_slug')">
|
||||
This slug is used to identify your {{ Spark::teamString() }} in URLs.
|
||||
</p>
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('team_slug')">
|
||||
@{{ registerForm.errors.get('team_slug') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<!-- Name -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('name')}">
|
||||
<label class="col-md-4 control-label">Name</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="name" v-model="registerForm.name" autofocus>
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('name')">
|
||||
@{{ registerForm.errors.get('name') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail Address -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('email')}">
|
||||
<label class="col-md-4 control-label">E-Mail Address</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="email" class="form-control" name="email" v-model="registerForm.email">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('email')">
|
||||
@{{ registerForm.errors.get('email') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('password')}">
|
||||
<label class="col-md-4 control-label">Password</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="password" class="form-control" name="password" v-model="registerForm.password">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('password')">
|
||||
@{{ registerForm.errors.get('password') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Confirmation -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('password_confirmation')}">
|
||||
<label class="col-md-4 control-label">Confirm Password</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="password" class="form-control" name="password_confirmation" v-model="registerForm.password_confirmation">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('password_confirmation')">
|
||||
@{{ registerForm.errors.get('password_confirmation') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms And Conditions -->
|
||||
<div v-if=" ! selectedPlan || selectedPlan.price == 0">
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('terms')}">
|
||||
<div class="col-md-6 col-md-offset-4">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="terms" v-model="registerForm.terms">
|
||||
I Accept The <a href="/terms" target="_blank">Terms Of Service</a>
|
||||
</label>
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('terms')">
|
||||
@{{ registerForm.errors.get('terms') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-6 col-md-offset-4">
|
||||
<button class="btn btn-primary" @click.prevent="register" :disabled="registerForm.busy">
|
||||
<span v-if="registerForm.busy">
|
||||
<i class="fa fa-btn fa-spinner fa-spin"></i>Registering
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<i class="fa fa-btn fa-check-circle"></i>Register
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
175
spark/resources/views/auth/register-common.blade.php
Normal file
175
spark/resources/views/auth/register-common.blade.php
Normal file
@@ -0,0 +1,175 @@
|
||||
<!-- Coupon -->
|
||||
<div class="row" v-if="coupon">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading">Discount</div>
|
||||
|
||||
<div class="panel-body">
|
||||
The coupon's @{{ discount }} discount will be applied to your subscription!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invalid Coupon -->
|
||||
<div class="row" v-if="invalidCoupon">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="alert alert-danger">
|
||||
Whoops! This coupon code is invalid.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invitation -->
|
||||
<div class="row" v-if="invitation">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="alert alert-success">
|
||||
We found your invitation to the <strong>@{{ invitation.team.name }}</strong> {{ Spark::teamString() }}!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invalid Invitation -->
|
||||
<div class="row" v-if="invalidInvitation">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="alert alert-danger">
|
||||
Whoops! This invitation code is invalid.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plan Selection -->
|
||||
<div class="row" v-if="paidPlans.length > 0">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="pull-left" :class="{'btn-table-align': hasMonthlyAndYearlyPlans}">
|
||||
Subscription
|
||||
</div>
|
||||
|
||||
<!-- Interval Selector Button Group -->
|
||||
<div class="pull-right">
|
||||
<div class="btn-group" v-if="hasMonthlyAndYearlyPlans" style="padding-top: 2px;">
|
||||
<!-- Monthly Plans -->
|
||||
<button type="button" class="btn btn-default"
|
||||
@click="showMonthlyPlans"
|
||||
:class="{'active': showingMonthlyPlans}">
|
||||
|
||||
Monthly
|
||||
</button>
|
||||
|
||||
<!-- Yearly Plans -->
|
||||
<button type="button" class="btn btn-default"
|
||||
@click="showYearlyPlans"
|
||||
:class="{'active': showingYearlyPlans}">
|
||||
|
||||
Yearly
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-body spark-row-list">
|
||||
<!-- Plan Error Message - In General Will Never Be Shown -->
|
||||
<div class="alert alert-danger" v-if="registerForm.errors.has('plan')">
|
||||
@{{ registerForm.errors.get('plan') }}
|
||||
</div>
|
||||
|
||||
<!-- European VAT Notice -->
|
||||
@if (Spark::collectsEuropeanVat())
|
||||
<p class="p-b-md">
|
||||
All subscription plan prices are excluding applicable VAT.
|
||||
</p>
|
||||
@endif
|
||||
|
||||
<table class="table table-borderless m-b-none">
|
||||
<thead></thead>
|
||||
<tbody>
|
||||
<tr v-for="plan in plansForActiveInterval">
|
||||
<!-- Plan Name -->
|
||||
<td>
|
||||
<div class="btn-table-align" @click="showPlanDetails(plan)">
|
||||
<span style="cursor: pointer;">
|
||||
<strong>@{{ plan.name }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Plan Features Button -->
|
||||
<td>
|
||||
<button class="btn btn-default m-l-sm" @click="showPlanDetails(plan)">
|
||||
<i class="fa fa-btn fa-star-o"></i>Plan Features
|
||||
</button>
|
||||
</td>
|
||||
|
||||
<!-- Plan Price -->
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
<span v-if="plan.price == 0">
|
||||
Free
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
@{{ plan.price | currency }} / @{{ plan.interval | capitalize }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Trial Days -->
|
||||
<td>
|
||||
<div class="btn-table-align" v-if="plan.trialDays">
|
||||
@{{ plan.trialDays}} Day Trial
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Plan Select Button -->
|
||||
<td class="text-right">
|
||||
<button class="btn btn-primary btn-plan" v-if="isSelected(plan)" disabled>
|
||||
<i class="fa fa-btn fa-check"></i>Selected
|
||||
</button>
|
||||
|
||||
<button class="btn btn-primary-outline btn-plan" @click="selectPlan(plan)" v-else>
|
||||
Select
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Profile -->
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span v-if="paidPlans.length > 0">
|
||||
Profile
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
Register
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<!-- Generic Error Message -->
|
||||
<div class="alert alert-danger" v-if="registerForm.errors.has('form')">
|
||||
@{{ registerForm.errors.get('form') }}
|
||||
</div>
|
||||
|
||||
<!-- Invitation Code Error -->
|
||||
<div class="alert alert-danger" v-if="registerForm.errors.has('invitation')">
|
||||
@{{ registerForm.errors.get('invitation') }}
|
||||
</div>
|
||||
|
||||
<!-- Registration Form -->
|
||||
@include('spark::auth.register-common-form')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
166
spark/resources/views/auth/register-stripe.blade.php
Normal file
166
spark/resources/views/auth/register-stripe.blade.php
Normal file
@@ -0,0 +1,166 @@
|
||||
@extends('spark::layouts.app')
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://js.stripe.com/v2/"></script>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<spark-register-stripe inline-template>
|
||||
<div>
|
||||
<div class="spark-screen container">
|
||||
<!-- Common Register Form Contents -->
|
||||
@include('spark::auth.register-common')
|
||||
|
||||
<!-- Billing Information -->
|
||||
<div class="row" v-if="selectedPlan && selectedPlan.price > 0">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Billing Information</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<!-- Generic 500 Level Error Message / Stripe Threw Exception -->
|
||||
<div class="alert alert-danger" v-if="registerForm.errors.has('form')">
|
||||
We had trouble validating your card. It's possible your card provider is preventing
|
||||
us from charging the card. Please contact your card provider or customer support.
|
||||
</div>
|
||||
|
||||
<form class="form-horizontal" role="form">
|
||||
<!-- Billing Address Fields -->
|
||||
@if (Spark::collectsBillingAddress())
|
||||
<h2><i class="fa fa-btn fa-map-marker"></i>Billing Address</h2>
|
||||
|
||||
@include('spark::auth.register-address')
|
||||
|
||||
<h2><i class="fa fa-btn fa-credit-card"></i>Credit Card</h2>
|
||||
@endif
|
||||
|
||||
<!-- Cardholder's Name -->
|
||||
<div class="form-group">
|
||||
<label for="name" class="col-md-4 control-label">Cardholder's Name</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="name" v-model="cardForm.name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card Number -->
|
||||
<div class="form-group" :class="{'has-error': cardForm.errors.has('number')}">
|
||||
<label class="col-md-4 control-label">Card Number</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="number" data-stripe="number" v-model="cardForm.number">
|
||||
|
||||
<span class="help-block" v-show="cardForm.errors.has('number')">
|
||||
@{{ cardForm.errors.get('number') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Security Code -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label">Security Code</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="cvc" data-stripe="cvc" v-model="cardForm.cvc">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expiration -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label">Expiration</label>
|
||||
|
||||
<!-- Month -->
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control" name="month"
|
||||
placeholder="MM" maxlength="2" data-stripe="exp-month" v-model="cardForm.month">
|
||||
</div>
|
||||
|
||||
<!-- Year -->
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control" name="year"
|
||||
placeholder="YYYY" maxlength="4" data-stripe="exp-year" v-model="cardForm.year">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ZIP Code -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('zip')}" v-if=" ! spark.collectsBillingAddress">
|
||||
<label class="col-md-4 control-label">ZIP / Postal Code</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="zip" v-model="registerForm.zip">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('zip')">
|
||||
@{{ registerForm.errors.get('zip') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coupon Code -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('coupon')}" v-if="query.coupon">
|
||||
<label class="col-md-4 control-label">Coupon Code</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="coupon" v-model="registerForm.coupon">
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('coupon')">
|
||||
@{{ registerForm.errors.get('coupon') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms And Conditions -->
|
||||
<div class="form-group" :class="{'has-error': registerForm.errors.has('terms')}">
|
||||
<div class="col-md-6 col-md-offset-4">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" v-model="registerForm.terms">
|
||||
I Accept The <a href="/terms" target="_blank">Terms Of Service</a>
|
||||
|
||||
<span class="help-block" v-show="registerForm.errors.has('terms')">
|
||||
<strong>@{{ registerForm.errors.get('terms') }}</strong>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tax / Price Information -->
|
||||
<div class="form-group" v-if="spark.collectsEuropeanVat && countryCollectsVat && selectedPlan">
|
||||
<label class="col-md-4 control-label"> </label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="alert alert-info" style="margin: 0;">
|
||||
<strong>Tax:</strong> @{{ taxAmount(selectedPlan) | currency }}
|
||||
<br><br>
|
||||
<strong>Total Price Including Tax:</strong>
|
||||
@{{ priceWithTax(selectedPlan) | currency }} / @{{ selectedPlan.interval | capitalize }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Button -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-6 col-md-offset-4">
|
||||
<button type="submit" class="btn btn-primary" @click.prevent="register" :disabled="registerForm.busy">
|
||||
<span v-if="registerForm.busy">
|
||||
<i class="fa fa-btn fa-spinner fa-spin"></i>Registering
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<i class="fa fa-btn fa-check-circle"></i>Register
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plan Features Modal -->
|
||||
@include('spark::modals.plan-details')
|
||||
</div>
|
||||
</spark-register-stripe>
|
||||
@endsection
|
5
spark/resources/views/auth/register.blade.php
Normal file
5
spark/resources/views/auth/register.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
@if (Spark::billsUsingStripe())
|
||||
@include('spark::auth.register-stripe')
|
||||
@else
|
||||
@include('spark::auth.register-braintree')
|
||||
@endif
|
43
spark/resources/views/auth/token.blade.php
Normal file
43
spark/resources/views/auth/token.blade.php
Normal file
@@ -0,0 +1,43 @@
|
||||
@extends('spark::layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Two-Factor Authentication</div>
|
||||
|
||||
<div class="panel-body">
|
||||
@include('spark::shared.errors')
|
||||
|
||||
<form class="form-horizontal" role="form" method="POST" action="/login/token">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<!-- Token -->
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label">Authentication Token</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="token" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verify Button -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-6 col-md-offset-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Verify
|
||||
</button>
|
||||
|
||||
<a class="btn btn-link" href="{{ url('login-via-emergency-token') }}">
|
||||
Lost Your Device?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
70
spark/resources/views/kiosk.blade.php
Normal file
70
spark/resources/views/kiosk.blade.php
Normal file
@@ -0,0 +1,70 @@
|
||||
@extends('spark::layouts.app')
|
||||
|
||||
@section('scripts')
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/mousetrap/1.4.6/mousetrap.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.4.0/Chart.min.js"></script>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<spark-kiosk :user="user" inline-template>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Tabs -->
|
||||
<div class="col-md-4">
|
||||
<div class="panel panel-default panel-flush">
|
||||
<div class="panel-heading">
|
||||
Kiosk
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="spark-settings-tabs">
|
||||
<ul class="nav spark-settings-stacked-tabs" role="tablist">
|
||||
<!-- Announcements Link -->
|
||||
<li role="presentation" class="active">
|
||||
<a href="#announcements" aria-controls="announcements" role="tab" data-toggle="tab">
|
||||
<i class="fa fa-fw fa-btn fa-bullhorn"></i>Announcements
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Metrics Link -->
|
||||
<li role="presentation">
|
||||
<a href="#metrics" aria-controls="metrics" role="tab" data-toggle="tab">
|
||||
<i class="fa fa-fw fa-btn fa-bar-chart"></i>Metrics
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Users Link -->
|
||||
<li role="presentation">
|
||||
<a href="#users" aria-controls="users" role="tab" data-toggle="tab">
|
||||
<i class="fa fa-fw fa-btn fa-user"></i>Users
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Panels -->
|
||||
<div class="col-md-8">
|
||||
<div class="tab-content">
|
||||
<!-- Announcements -->
|
||||
<div role="tabpanel" class="tab-pane active" id="announcements">
|
||||
@include('spark::kiosk.announcements')
|
||||
</div>
|
||||
|
||||
<!-- Metrics -->
|
||||
<div role="tabpanel" class="tab-pane" id="metrics">
|
||||
@include('spark::kiosk.metrics')
|
||||
</div>
|
||||
|
||||
<!-- User Management -->
|
||||
<div role="tabpanel" class="tab-pane" id="users">
|
||||
@include('spark::kiosk.users')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</spark-kiosk>
|
||||
@endsection
|
212
spark/resources/views/kiosk/announcements.blade.php
Normal file
212
spark/resources/views/kiosk/announcements.blade.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<spark-kiosk-announcements inline-template>
|
||||
<div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Create Announcement</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="alert alert-info">
|
||||
Announcements you create here will be sent to the "Product Announcements" section of
|
||||
the notifications modal window, informing your users about new features and improvements
|
||||
to your application.
|
||||
</div>
|
||||
|
||||
<form class="form-horizontal" role="form">
|
||||
<!-- Announcement -->
|
||||
<div class="form-group" :class="{'has-error': createForm.errors.has('body')}">
|
||||
<label class="col-md-4 control-label">Announcement</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<textarea class="form-control" name="announcement" rows="7" v-model="createForm.body" style="font-family: monospace;">
|
||||
</textarea>
|
||||
|
||||
<span class="help-block" v-show="createForm.errors.has('body')">
|
||||
@{{ createForm.errors.get('body') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Text -->
|
||||
<div class="form-group" :class="{'has-error': createForm.errors.has('action_text')}">
|
||||
<label class="col-md-4 control-label">Action Button Text</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="action_text" v-model="createForm.action_text">
|
||||
|
||||
<span class="help-block" v-show="createForm.errors.has('action_text')">
|
||||
@{{ createForm.errors.get('action_text') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action URL -->
|
||||
<div class="form-group" :class="{'has-error': createForm.errors.has('action_url')}">
|
||||
<label class="col-md-4 control-label">Action Button URL</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="action_url" v-model="createForm.action_url">
|
||||
|
||||
<span class="help-block" v-show="createForm.errors.has('action_url')">
|
||||
@{{ createForm.errors.get('action_url') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Button -->
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-4 col-md-6">
|
||||
<button type="submit" class="btn btn-primary"
|
||||
@click.prevent="create"
|
||||
:disabled="createForm.busy">
|
||||
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Announcements List -->
|
||||
<div class="panel panel-default" v-if="announcements.length > 0">
|
||||
<div class="panel-heading">Recent Announcements</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<table class="table table-borderless m-b-none">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th>Date</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="announcement in announcements">
|
||||
<!-- Photo -->
|
||||
<td>
|
||||
<img :src="announcement.creator.photo_url" class="spark-profile-photo">
|
||||
</td>
|
||||
|
||||
<!-- Date -->
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
@{{ announcement.created_at | datetime }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Edit Button -->
|
||||
<td>
|
||||
<button class="btn btn-primary" @click="editAnnouncement(announcement)">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<td>
|
||||
<button class="btn btn-danger-outline" @click="approveAnnouncementDelete(announcement)">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Announcement Modal -->
|
||||
<div class="modal fade" id="modal-update-announcement" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" v-if="updatingAnnouncement">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button " class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
|
||||
<h4 class="modal-title">
|
||||
Update Announcement
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Update Announcement -->
|
||||
<form class="form-horizontal" role="form">
|
||||
<!-- Announcement -->
|
||||
<div class="form-group" :class="{'has-error': updateForm.errors.has('body')}">
|
||||
<label class="col-md-4 control-label">Announcement</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<textarea class="form-control" rows="7" v-model="updateForm.body" style="font-family: monospace;">
|
||||
</textarea>
|
||||
|
||||
<span class="help-block" v-show="updateForm.errors.has('body')">
|
||||
@{{ updateForm.errors.get('body') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Text -->
|
||||
<div class="form-group" :class="{'has-error': updateForm.errors.has('action_text')}">
|
||||
<label class="col-md-4 control-label">Action Button Text</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="action_text" v-model="updateForm.action_text">
|
||||
|
||||
<span class="help-block" v-show="updateForm.errors.has('action_text')">
|
||||
@{{ updateForm.errors.get('action_text') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action URL -->
|
||||
<div class="form-group" :class="{'has-error': updateForm.errors.has('action_url')}">
|
||||
<label class="col-md-4 control-label">Action Button URL</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" name="action_url" v-model="updateForm.action_url">
|
||||
|
||||
<span class="help-block" v-show="updateForm.errors.has('action_url')">
|
||||
@{{ updateForm.errors.get('action_url') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" @click="update" :disabled="updateForm.busy">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Announcement Modal -->
|
||||
<div class="modal fade" id="modal-delete-announcement" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" v-if="deletingAnnouncement">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button " class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
|
||||
<h4 class="modal-title">
|
||||
Delete Announcement
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete this announcement?
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">No, Go Back</button>
|
||||
|
||||
<button type="button" class="btn btn-danger" @click="deleteAnnouncement" :disabled="deleteForm.busy">
|
||||
Yes, Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</spark-kiosk-announcements>
|
197
spark/resources/views/kiosk/metrics.blade.php
Normal file
197
spark/resources/views/kiosk/metrics.blade.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<spark-kiosk-metrics :user="user" inline-template>
|
||||
<!-- The Landsman™ -->
|
||||
<div>
|
||||
<div class="row">
|
||||
<!-- Monthly Recurring Revenue -->
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading text-center">Monthly Recurring Revenue</div>
|
||||
|
||||
<div class="panel-body text-center">
|
||||
<div style="font-size: 24px;">
|
||||
@{{ monthlyRecurringRevenue | currency }}
|
||||
</div>
|
||||
|
||||
<!-- Compared To Last Month -->
|
||||
<div v-if="monthlyChangeInMonthlyRecurringRevenue" style="font-size: 12px;">
|
||||
@{{ monthlyChangeInMonthlyRecurringRevenue }}% From Last Month
|
||||
</div>
|
||||
|
||||
<!-- Compared To Last Year -->
|
||||
<div v-if="yearlyChangeInMonthlyRecurringRevenue" style="font-size: 12px;">
|
||||
@{{ yearlyChangeInMonthlyRecurringRevenue }}% From Last Year
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Yearly Recurring Revenue -->
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading text-center">Yearly Recurring Revenue</div>
|
||||
|
||||
<div class="panel-body text-center">
|
||||
<div style="font-size: 24px;">
|
||||
@{{ yearlyRecurringRevenue | currency }}
|
||||
</div>
|
||||
|
||||
<!-- Compared To Last Month -->
|
||||
<div v-if="monthlyChangeInYearlyRecurringRevenue" style="font-size: 12px;">
|
||||
@{{ monthlyChangeInYearlyRecurringRevenue }}% From Last Month
|
||||
</div>
|
||||
|
||||
<!-- Compared To Last Year -->
|
||||
<div v-if="yearlyChangeInYearlyRecurringRevenue" style="font-size: 12px;">
|
||||
@{{ yearlyChangeInYearlyRecurringRevenue }}% From Last Year
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Total Volume -->
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-success">
|
||||
<div class="panel-heading text-center">Total Volume</div>
|
||||
|
||||
<div class="panel-body text-center">
|
||||
<span style="font-size: 24px;">
|
||||
@{{ totalVolume | currency }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Trials -->
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-info">
|
||||
@if(Spark::teamTrialDays())
|
||||
<div class="panel-heading text-center">Teams Currently Trialing</div>
|
||||
@else
|
||||
<div class="panel-heading text-center">Users Currently Trialing</div>
|
||||
@endif
|
||||
|
||||
<div class="panel-body text-center">
|
||||
<span style="font-size: 24px;">
|
||||
@{{ totalTrialUsers }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Monthly Recurring Revenue Chart -->
|
||||
<div class="row" v-show="indicators.length > 0">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Monthly Recurring Revenue</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<canvas id="monthlyRecurringRevenueChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Yearly Recurring Revenue Chart -->
|
||||
<div class="row" v-show="indicators.length > 0">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Yearly Recurring Revenue</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<canvas id="yearlyRecurringRevenueChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-show="indicators.length > 0">
|
||||
<!-- Daily Volume Chart -->
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Daily Volume</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<canvas id="dailyVolumeChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Daily New Users Chart -->
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">New Users</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<canvas id="newUsersChart" height="100"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Subscribers Per Plan -->
|
||||
<div class="row" v-if="plans.length > 0">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Subscribers</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<table class="table table-borderless m-b-none">
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Subscribers</th>
|
||||
<th>Trialing</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-if="genericTrialUsers">
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
On Generic Trial
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
N/A
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
@{{ genericTrialUsers }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="plan in plans">
|
||||
<!-- Plan Name -->
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
@{{ plan.name }} (@{{ plan.interval | capitalize }})
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Subscriber Count -->
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
@{{ plan.count }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Trialing Count -->
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
@{{ plan.trialing }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</spark-kiosk-metrics>
|
127
spark/resources/views/kiosk/modals/add-discount.blade.php
Normal file
127
spark/resources/views/kiosk/modals/add-discount.blade.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<spark-kiosk-add-discount inline-template>
|
||||
<div>
|
||||
<div class="modal fade" id="modal-add-discount" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" v-if="discountingUser">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button " class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
|
||||
<h4 class="modal-title">
|
||||
Add Discount (@{{ discountingUser.name }})
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<!-- Current Discount -->
|
||||
<div class="alert alert-success" v-if="currentDiscount">
|
||||
This user has a discount of @{{ formattedDiscount(currentDiscount) }}
|
||||
for @{{ formattedDiscountDuration(currentDiscount) }}.
|
||||
</div>
|
||||
|
||||
<!-- Add Discount Form -->
|
||||
<form class="form-horizontal" role="form">
|
||||
<!-- Discount Type -->
|
||||
<div class="form-group" :class="{'has-error': form.errors.has('type')}">
|
||||
<label class="col-sm-4 control-label">Discount Type</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" value="amount" v-model="form.type"> Amount
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" value="percent" v-model="form.type"> Percentage
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<span class="help-block" v-show="form.errors.has('type')">
|
||||
@{{ form.errors.get('type') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discount Value -->
|
||||
<div class="form-group" :class="{'has-error': form.errors.has('value')}">
|
||||
<label class="col-md-4 control-label">
|
||||
<span v-if="form.type == 'percent'">Percentage</span>
|
||||
|
||||
<span v-if="form.type == 'amount'">Amount</span>
|
||||
</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" v-model="form.value">
|
||||
|
||||
<span class="help-block" v-show="form.errors.has('value')">
|
||||
@{{ form.errors.get('value') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discount Duration -->
|
||||
<div class="form-group" :class="{'has-error': form.errors.has('duration')}">
|
||||
<label class="col-sm-4 control-label">Discount Duration</label>
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" value="once" v-model="form.duration"> Once
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" value="forever" v-model="form.duration"> Forever
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" value="repeating" v-model="form.duration"> Multiple Months
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<span class="help-block" v-show="form.errors.has('duration')">
|
||||
@{{ form.errors.get('duration') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Duration Months -->
|
||||
<div class="form-group" :class="{'has-error': form.errors.has('months')}" v-if="form.duration == 'repeating'">
|
||||
<label class="col-md-4 control-label">
|
||||
Months
|
||||
</label>
|
||||
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" v-model="form.months">
|
||||
|
||||
<span class="help-block" v-show="form.errors.has('months')">
|
||||
@{{ form.errors.get('months') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Modal Actions -->
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
|
||||
<button type="button" class="btn btn-primary" @click="applyDiscount" :disabled="form.busy">
|
||||
<span v-if="form.busy">
|
||||
<i class="fa fa-btn fa-spinner fa-spin"></i>Applying
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
Apply Discount
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</spark-kiosk-add-discount>
|
148
spark/resources/views/kiosk/profile.blade.php
Normal file
148
spark/resources/views/kiosk/profile.blade.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<spark-kiosk-profile :user="user" :plans="plans" inline-template>
|
||||
<div>
|
||||
<!-- Loading Indicator -->
|
||||
<div class="row" v-if="loading">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<i class="fa fa-btn fa-spinner fa-spin"></i>Loading
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Profile -->
|
||||
<div v-if=" ! loading && profile">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<!-- User Name -->
|
||||
<div class="pull-left">
|
||||
<div class="btn-table-align">
|
||||
<i class="fa fa-btn fa-times" style="cursor: pointer;" @click="showSearch"></i>
|
||||
@{{ profile.name }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profile Actions -->
|
||||
<div class="pull-right" style="padding-top: 2px;">
|
||||
<div class="btn-group" role="group">
|
||||
<!-- Apply Discount -->
|
||||
<button class="btn btn-default" v-if="spark.usesStripe && profile.stripe_id" @click="addDiscount(profile)">
|
||||
<i class="fa fa-gift"></i>
|
||||
</button>
|
||||
|
||||
<!-- Impersonate Button -->
|
||||
<button class="btn btn-default" @click="impersonate(profile)" :disabled="user.id === profile.id">
|
||||
<i class="fa fa-user-secret"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<!-- Profile Photo -->
|
||||
<div class="col-md-3 text-center">
|
||||
<img :src="profile.photo_url" class="spark-profile-photo-xl">
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<!-- Email Address -->
|
||||
<p>
|
||||
<strong>Email Address:</strong> <a :href="'mailto:'+profile.email">@{{ profile.email }}</a>
|
||||
</p>
|
||||
|
||||
<!-- Joined Date -->
|
||||
<p>
|
||||
<strong>Joined:</strong> @{{ profile.created_at | datetime }}
|
||||
</p>
|
||||
|
||||
<!-- Subscription -->
|
||||
<p>
|
||||
<strong>Subscription:</strong>
|
||||
|
||||
<span v-if="activePlan(profile)">
|
||||
<a :href="customerUrlOnBillingProvider(profile)" target="_blank">
|
||||
@{{ activePlan(profile).name }} (@{{ activePlan(profile).interval | capitalize }})
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
None
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- Total Revenue -->
|
||||
<p>
|
||||
<strong>Total Revenue:</strong> @{{ revenue | currency(spark.currencySymbol) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Teams -->
|
||||
<div class="row" v-if="spark.usesTeams && profile.owned_teams.length > 0">
|
||||
<div class="col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{{ ucfirst(str_plural(Spark::teamString())) }}
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<table class="table table-borderless m-b-none">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>Subscription</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="team in profile.owned_teams">
|
||||
<!-- Photo -->
|
||||
<td>
|
||||
<img :src="team.photo_url" class="spark-team-photo">
|
||||
</td>
|
||||
|
||||
<!-- Team Name -->
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
@{{ team.name }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Subscription -->
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
<span v-if="activePlan(team)">
|
||||
<a :href="customerUrlOnBillingProvider(team)" target="_blank">
|
||||
@{{ activePlan(team).name }} (@{{ activePlan(team).interval | capitalize }})
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
None
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apply Discount Modal -->
|
||||
<div>
|
||||
@include('spark::kiosk.modals.add-discount')
|
||||
</div>
|
||||
</div>
|
||||
</spark-kiosk-profile>
|
92
spark/resources/views/kiosk/users.blade.php
Normal file
92
spark/resources/views/kiosk/users.blade.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<spark-kiosk-users :user="user" inline-template>
|
||||
<div>
|
||||
<div v-show=" ! showingUserProfile">
|
||||
<!-- Search Field Panel -->
|
||||
<div class="panel panel-default panel-flush" style="border: 0;">
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal p-b-none" role="form" @submit.prevent>
|
||||
<!-- Search Field -->
|
||||
<div class="form-group m-b-none">
|
||||
<div class="col-md-12">
|
||||
<input type="text" id="kiosk-users-search" class="form-control"
|
||||
name="search"
|
||||
placeholder="Search By Name Or E-Mail Address..."
|
||||
v-model="searchForm.query"
|
||||
@keyup.enter="search">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Searching -->
|
||||
<div class="panel panel-default" v-if="searching">
|
||||
<div class="panel-heading">Search Results</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<i class="fa fa-btn fa-spinner fa-spin"></i>Searching
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Search Results -->
|
||||
<div class="panel panel-warning" v-if=" ! searching && noSearchResults">
|
||||
<div class="panel-heading">Search Results</div>
|
||||
|
||||
<div class="panel-body">
|
||||
No users matched the given criteria.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Search Results -->
|
||||
<div class="panel panel-default" v-if=" ! searching && searchResults.length > 0">
|
||||
<div class="panel-heading">Search Results</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<table class="table table-borderless m-b-none">
|
||||
<thead>
|
||||
<th></th>
|
||||
<th>Name</th>
|
||||
<th>E-Mail Address</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="searchUser in searchResults">
|
||||
<!-- Profile Photo -->
|
||||
<td>
|
||||
<img :src="searchUser.photo_url" class="spark-profile-photo">
|
||||
</td>
|
||||
|
||||
<!-- Name -->
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
@{{ searchUser.name }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- E-Mail Address -->
|
||||
<td>
|
||||
<div class="btn-table-align">
|
||||
@{{ searchUser.email }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<!-- View User Profile -->
|
||||
<button class="btn btn-default" @click="showUserProfile(searchUser)">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Profile Detail -->
|
||||
<div v-show="showingUserProfile">
|
||||
@include('spark::kiosk.profile')
|
||||
</div>
|
||||
</div>
|
||||
</spark-kiosk-users>
|
53
spark/resources/views/layouts/app.blade.php
Normal file
53
spark/resources/views/layouts/app.blade.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Meta Information -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>@yield('title', config('app.name'))</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300,400,600' rel='stylesheet' type='text/css'>
|
||||
<link href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css' rel='stylesheet' type='text/css'>
|
||||
|
||||
<!-- CSS -->
|
||||
<link href="/css/sweetalert.css" rel="stylesheet">
|
||||
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
|
||||
|
||||
<!-- Scripts -->
|
||||
@yield('scripts', '')
|
||||
|
||||
<!-- Global Spark Object -->
|
||||
<script>
|
||||
window.Spark = <?php echo json_encode(array_merge(
|
||||
Spark::scriptVariables(), []
|
||||
)); ?>;
|
||||
</script>
|
||||
</head>
|
||||
<body class="with-navbar">
|
||||
<div id="spark-app" v-cloak>
|
||||
<!-- Navigation -->
|
||||
@if (Auth::check())
|
||||
@include('spark::nav.user')
|
||||
@else
|
||||
@include('spark::nav.guest')
|
||||
@endif
|
||||
|
||||
<!-- Main Content -->
|
||||
@yield('content')
|
||||
|
||||
<!-- Application Level Modals -->
|
||||
@if (Auth::check())
|
||||
@include('spark::modals.notifications')
|
||||
@include('spark::modals.support')
|
||||
@include('spark::modals.session-expired')
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="{{ mix('js/app.js') }}"></script>
|
||||
<script src="/js/sweetalert.min.js"></script>
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user