Adding checkout functionality to an eCommerce page
function ProductPurchaser(opts) {
var self = this;
opts = opts || {};
var $checkoutBtn;
/**
* Represents the logic for the eCommerce / Snipcart checkout page
* @constructor
*/
var init = function () {
$checkoutBtn = $('.btn-buy-these-products');
initCartCurrency();
self.animateImagesForProductModal();
self.handleItemRemovedFromCartViaSnipcartModal();
self.handleItemsAlreadyInCartUponPageLoad();
self.bindAddBtns();
self.handleCartOpened();
self.handleCartOrderComplete();
bindZeroTotalCheckoutIndicator();
updateFooterText();
};
/**
* Set the cart's currency based on the building's country
*/
var initCartCurrency = function() {
if (opts.building.country == 'CA') {
Snipcart.api.cart.currency('cad');
} else {
Snipcart.api.cart.currency('usd');
}
};
/**
* Snipcart is cool with us removing their mention from the footer, so
* why not?
*/
var updateFooterText = function() {
Snipcart.subscribe('cart.opened', function() {
$('.snip-footer__highlight').text(' company X');
});
};
/**
* If the cart total is $0, then adds a message to tell the user
* that they won't be charged.
*/
var bindZeroTotalCheckoutIndicator = function() {
Snipcart.subscribe('page.change', function (page) {
if (page != 'payment-method') return; // Skip unless we're on the payment method page
if (Snipcart.api.cart.get().total != 0) return; // Skip unless the total is 0
// Do this in a timer to let the Ajax load complete.
// TODO: We'll probably want to be smarter about waiting for Snipcart's content to load in the future.
setTimeout(function() {
var $note = $('#ex_checkout_notice');
if ($note.length > 0) return; // Skip if it's already there
$('#snip-layout-payment-method .snipcart-step').prepend('<div class="ex-checkout-notice" id="ex_checkout_notice">Note: <b>You will not be charged anything</b>. This information is just needed to process your order.</div>');
}, 250);
});
};
/**
* Updates the page display for total price and the total expected annual savings.
*/
self.updateTotals = function () {
var totalPrice = 0;
var numItems = 0;
Snipcart.api.items.all().forEach(function (el, idx, arr) {
totalPrice += parseFloat(el.totalPrice, 10);
numItems++;
});
$("span.order-total-amount").text('$' + totalPrice.formatMoney(2, '.', ','));
// Let's tell the UI whether or not there are any items in the cart so we can display accordingly:
if (numItems > 0) {
$checkoutBtn.addClass('has-items');
} else {
$checkoutBtn.removeClass('has-items');
}
var totalSavings = 0;
Snipcart.api.items.all().forEach(function (el, idx, arr) {
var savings = parseFloat($('button[data-item-id="' + el.id + '"]').data('annual-savings'), 10);
totalSavings += savings;
});
$("span.savings-details-amount-number").text('$' + totalSavings.formatMoney(2, '.', ','));
};
/**
* Remove an item from the user's Snipcart shopping cart.
* @param {string} itemId - The SKU of the item.
*/
self.removeItemFromSnipcart = function (itemId) {
LoadMask.show();
Snipcart.api.items.remove(itemId).then(function (item) {
$('button[data-item-id="' + itemId + '"]').toArray().forEach(function (el, idx, arr) {
$(el).removeClass('selected');
});
self.updateTotals();
LoadMask.hide();
});
};
/**
* Event handling for a user opening the Snipcart checkout dialog
*/
self.handleCartOpened = function() {
Snipcart.subscribe('cart.opened', function() {
self.updateAnalytics('cartOpened');
});
};
/**
* Event handling for a user completing order from Snipcart dialog,
* including payment processing.
*/
self.handleCartOrderComplete = function() {
Snipcart.subscribe('order.completed', function (data) {
self.updateAnalytics('completedCheckout');
});
};
/**
* Adds an item to the user's Snipcart shopping cart.
* @param {string} itemId - The SKU of the item.
* @param {string} itemName - The name of the item.
* @param {string} itemDescription - The description of the item.
* @param {string} itemUrl - The publicly-accessible URL where the item's button can be viewed; for Snipcart security.
* @param {string} itemPrice - The price of the item.
*/
self.addItemToSnipcart = function (itemId, itemName, itemDescription, itemUrl, itemPrice, opts) {
opts = opts || {};
LoadMask.show();
Snipcart.api.items.add({
"id": itemId,
"name": itemName,
"description": itemDescription,
"url": itemUrl,
"price": itemPrice,
"quantity": 1,
"stackable": false,
"customFields": []
}).then(function (item) {
$('button[data-item-id="' + itemId + '"]').toArray().forEach(function (el, idx, arr) {
$(el).addClass('selected');
});
self.updateTotals();
self.updateAnalytics('itemAdded');
LoadMask.hide();
if (window.isMobile()) {
self.suggestCheckout(opts);
}
if (typeof opts.afterAddToCart === 'function') opts.afterAddToCart();
});
};
/**
* Urges user to Checkout Now, after adding an item to cart.
* Only shows for users on mobile devices.
*/
self.suggestCheckout = function(opts) {
swal({
title: 'Product selected!',
text: "Ready to checkout?",
type: 'success',
showCancelButton: true,
confirmButtonText: 'Checkout Now',
cancelButtonText: opts.cancelButtonText || 'Keep Shopping'
}).then(function () {
Snipcart.api.modal.show();
}, function (dismiss) {
// dismiss can be 'cancel', 'overlay',
// 'close', and 'timer'
});
};
/**
* Updates server-side analytics to keep track of user ecommerce behavior
* @param {string} analyticsEvent - What sort of event to track
*/
self.updateAnalytics = function(analyticsEvent) {
$.ajax( {
type: 'POST',
url: '/savings_plan/analytics',
beforeSend: function(xhr){
xhr.setRequestHeader('X-CSRF-Token', AUTH_TOKEN);
},
data: { analytics_event: analyticsEvent }
} );
};
/**
* Intercept DOM events to "Add to cart" button, irrespective of whether item is already in cart.
*/
self.bindAddBtns = function ($el, opts) {
$el = $el || $(".snipcart-add-item-modified");
opts = opts || {};
$el.click(function () {
var itemId = $(this).data('item-id').toString();
var itemName = $(this).data('item-name').toString();
var itemPrice = JSON.stringify($(this).data('item-price'));
var itemDescription = $(this).data('item-description').toString();
var itemUrl = $(this).data('item-url').toString();
var itemAlreadyInCart = false;
Snipcart.api.items.all().forEach(function (el, idx, arr) {
if (el.id === itemId) { // it is in cart. So let's remove it.
itemAlreadyInCart = true;
}
});
if (itemAlreadyInCart) {
self.removeItemFromSnipcart(itemId);
}
else {
self.addItemToSnipcart(itemId, itemName, itemDescription, itemUrl, itemPrice, opts);
}
});
};
/**
* Maybe the user refreshed the page and already has items in the cart.
* So, let's mark them "selected" on the page and update the totals.
*/
self.handleItemsAlreadyInCartUponPageLoad = function () {
var updateBtnsForProdsInCart = function () {
Snipcart.api.items.all().forEach(function (el, idx, arr) {
$('button[data-item-id="' + el.id + '"]').toArray().forEach(function (el, idx, arr) {
$(el).addClass('selected');
});
});
self.updateTotals();
};
if (Snipcart.ready) {
updateBtnsForProdsInCart();
} else {
Snipcart.subscribe('cart.ready', updateBtnsForProdsInCart);
}
};
/**
* Handles a user removing an item from the cart via the Snipcart modal itself.
*/
self.handleItemRemovedFromCartViaSnipcartModal = function () {
Snipcart.subscribe('item.removed', function (item) {
$('button[data-item-id="' + item.id + '"]').toArray().forEach(function (el, idx, arr) {
$(el).removeClass('selected');
});
self.updateTotals();
});
};
/**
* Handles mouseclick hotswapping of images within the product modal.
*/
self.animateImagesForProductModal = function () {
// TODO: This JS is super inefficient, let's optimize it.
$(".bottom-images .image").click(function () {
var $mainImg = $('.main-image img');
var mainImageUrl = $mainImg.attr('src');
var clickedImageUrl = $(this).find('img').attr('src');
$mainImg.attr('src', clickedImageUrl);
$(this).find('img').attr('src', mainImageUrl);
});
};
init();
};
Problem: My client's gig marketplace suffered from a bottleneck: not enough users were available to service their customers' demand. Consequently, jobs were not being completed and my client's customers were disappointed.
Solution: I implemented a node.js application that provided automatic notifications to my client's users (via email and in-app alerts) regarding jobs in their area. As a result, job completion rate went from under 50% to over 80%, and the business became gross profitable.
Web Application - SaaS
Company that provides services for developers.
I was and am the project owner. I sourced a designer, who created the design (PSD) and who implemented the HTML and CSS. I handled the rest, including building a series of Ruby scripts that source jobs for freelance programmers. I also handled the end-to-end rollout of the application and the marketing.