Skip to content

Instantly share code, notes, and snippets.

@rebolyte
Created October 2, 2017 18:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rebolyte/90105dc09b2bd4895d0c0c581a7d466f to your computer and use it in GitHub Desktop.
Save rebolyte/90105dc09b2bd4895d0c0c581a7d466f to your computer and use it in GitHub Desktop.
Shopping cart exercise
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="description" content="Make tacos in the browser!">
<meta name="keywords" content="taco, tacos, truck, austin">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shopping cart</title>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.3/css/bulma.min.css">
<style type="text/css">
[v-cloak] {
display: none;
}
html {
position: relative;
min-height: 100%;
}
body {
/* margin bottom by footer height */
margin-bottom: 9rem;
}
.link {
text-decoration: underline;
cursor: pointer;
}
.strike {
text-decoration: line-through;
}
.sticky-footer {
position: absolute;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<nav class="nav has-shadow">
<div class="container">
<div class="nav-left">
<span class="nav-item"><strong>CART</strong></span>
<a class="nav-item is-tab" active-class="is-active">Home</a>
<a class="nav-item is-tab" active-class="is-active">Customize</a>
<a class="nav-item is-tab" active-class="is-active">Check out</a>
</div>
<div class="nav-right">
<a class="nav-item is-tab">Account</a>
<a class="nav-item is-tab">Log In</a>
</div>
</div>
</nav>
<section class="section">
<div class="container">
<h1 class="title">Shopping cart</h1>
<h2 class="subtitle">Select your items and then submit your order</h2>
<div id="app" class="columns" v-cloak>
<div class="column is-two-thirds tile is-ancestor is-vertical"> <!-- is-vertical -->
<div v-if="loading">Loading...</div>
<app-item-selector v-for="item in items" :key="item.id" :item="item" :in-cart="false" @add-item="addItem($event)"></app-item-selector>
</div>
<div class="column">
<app-order-display
:orders="orders"
:discounts="discounts"
@delete-order="deleteOrder($event)"
@update-quant-cart="updateQuantity($event)"></app-order-display>
</div>
</div>
</div>
</section>
<footer class="footer sticky-footer">
<div class="container">
<div class="content has-text-centered">
<p><strong>Shopping cart example</strong>, built in 2.5 hours, &copy; rebolyte 2017</p>
</div>
</div>
</footer>
<script>
// thanks @davidgilbertson
var scripts = [
'https://unpkg.com/vue@2.4.4/dist/vue.js',
// 'https://unpkg.com/axios@0.16.1/dist/axios.min.js',
// 'https://wzrd.in/standalone/nanoid@latest',
'https://cdn.jsdelivr.net/npm/lodash@4.17.4/lodash.min.js',
'main.js'
];
var newBrowser = (
'Promise' in window &&
'assign' in Object
);
if (!newBrowser) {
console.log('goddamnit IE...');
scripts.unshift(
'https://unpkg.com/es6-object-assign@1.1.0/dist/object-assign.min.js',
'https://unpkg.com/native-promise-only@0.8.1/lib/npo.src.js'
)
}
scripts.forEach(function (src) {
var scriptEl = document.createElement('script');
scriptEl.src = src;
// https://www.html5rocks.com/en/tutorials/speed/script-loading/
scriptEl.async = false;
document.head.appendChild(scriptEl);
});
</script>
</body>
</html>
/* global _, Vue, console */
(function () {
'use strict';
// --- "EXTERNAL" MODULES ----------------------------
// iife standing in for real modules
window.config = (function () {
return {
items: [
{
id: 1,
name: 'Star Wars Episode IV DVD',
price: 20,
quantity: 1
},
{
id: 2,
name: 'Star Wars Episode IV Blu-Ray',
price: 25,
quantity: 1
},
{
id: 3,
name: 'Star Wars Episode V DVD',
price: 20,
quantity: 1
},
{
id: 4,
name: 'Star Wars Episode V Blu-Ray',
price: 25,
quantity: 1
},
{
id: 5,
name: 'Star Wars Episode VI DVD',
price: 20,
quantity: 1
},
{
id: 6,
name: 'Star Wars Episode VI Blu-Ray',
price: 25,
quantity: 1
}
],
discounts: [
{
// When the user adds all the Star Wars DVDs, they will automatically receive a 10% discount on
// those items (but not on any others).
name: 'All Star Wars DVDs - 10% off those items',
condition: _.partial(itemsPresent, [1, 3, 5]),
result: _.partial(applyDiscount, [1, 3, 5], 10),
priority: 2
},{
// When the user adds all the Star Wars Blu-Rays, they will automatically receive a 15% discount on
// those items (but not on any others).
name: 'All Star Wars Blu-Ray - 15% off those items',
condition: _.partial(itemsPresent, [2, 4, 6]),
result: _.partial(applyDiscount, [2, 4, 6], 15),
priority: 2
},{
// When the user buys 100 items or more (any items in any combination), they will automatically receive a
// 5% bulk discount on the cart total. This should get applied after the DVD and Blu-Ray discounts.
name: 'Bulk - 5% off your total',
condition: _.partial(itemCount, 100),
result: _.partial(applyDiscount, [], 5),
priority: -Infinity
}
]
};
}());
const apiService = (function () {
function getItems() {
return Promise.resolve(config.items); // stand-in for retrieving things from a back-end
}
function getDiscounts() {
return Promise.resolve(config.discounts);
}
return {
getItems,
getDiscounts
};
}());
// --- UTILS ----------------------------
function itemsPresent(itemIds, collec) {
// return true if all itemIds in collec
return _.intersection(itemIds, collec.map(o => o.id)).length === itemIds.length;
}
function itemCount(count, collec) {
// return true if number of items > count
let quant = collec.reduce((acc, cur) => {
return acc + (cur.quantity);
}, 0);
return quant >= count;
}
function applyDiscount(itemIds, percent, collec) {
// if anything in itemIds, apply percent discount to those
// otherwise apply percent dicount to whole collec
const minusDiscount = price => {
let discount = price * (percent / 100);
return { price: price - discount };
};
if (!_.isEmpty(itemIds)) {
return collec.map(item => {
return itemIds.includes(item.id) ? { ...item, ...minusDiscount(item.price) } : item;
});
} else {
return collec.map(item => {
return { ...item, ...minusDiscount(item.price) };
});
}
}
// --- COMPONENTS -----------------------
let ItemSelector = {
props: ['item', 'inCart'],
template: `
<div class="tile is-child notification is-light">
<div class="field">
<label class="label">{{ item.name }}</label>
<div>
Price: $ {{ item.price }}
</div>
<span class="link" @click="onAdd(item.id)">
Add to Cart
</span>
</div>
</div>
`,
methods: {
onAdd(v) {
this.$emit('add-item', v);
}
}
};
let CartItem = {
props: ['item'],
template: `
<div class="tile is-child notification is-light">
<div class="field">
<label class="label">{{ item.name }}</label>
<div>
Price: $ {{ item.price }}
</div>
<label for="">Quantity</label>
<input type="number" class="input" v-model="item.quantity" @change="quantChange($event.target.value)" min="1" />
<br>
<span class="link" @click="onRemove(item.id)">
Delete
</span>
</div>
</div>
`,
methods: {
onRemove(v) {
this.$emit('delete-item', v);
},
quantChange(v) {
this.$emit('update-quantity', parseInt(v, 10));
}
}
};
let OrderDisplay = {
props: ['orders', 'discounts'],
template: `
<div>
<h3 class="subtitle">Cart</h3>
<div class="tile is-ancestor">
<div class="tile is-vertical">
<app-cart-item v-for="item in orders"
:key="item.id"
:item="item"
:in-cart="true"
@delete-item="remove(item.id)"
@update-quantity="updateQuantity(item.id, $event)">
</app-cart-item>
<div class="tile is-child notification is-light" v-if="orders.length === 0">
<p>Nothing yet!</p>
</div>
</div>
</div>
<strong>Discounts:</strong>
<ul>
<li v-for="disc in appliedDiscounts">{{ disc.name }}</li>
</ul>
<strong>Total:</strong> $ <span class="strike" v-if="appliedDiscounts.length > 0">{{ origTotal | money }}</span> {{ total | money }}
<br>
<button type="button" class="button is-primary" :disabled="orders.length === 0">Check Out</button>
</div>
`,
components: {
'app-cart-item': CartItem
},
computed: {
origTotal() {
return this.orders.reduce((acc, cur) => {
return acc + (cur.price * cur.quantity);
}, 0);
},
total() {
let _orders = _.clone(this.orders);
let discs = _.sortBy(this.appliedDiscounts, 'priority').reverse();
let processor = _.flow(discs.map(d => d.result));
// console.log(discs);
return processor(_orders).reduce((acc, cur) => {
return acc + (cur.price * cur.quantity);
}, 0);
},
totalQuantity() {
return this.orders.reduce((acc, cur) => {
return acc + (cur.quantity);
}, 0);
},
appliedDiscounts() {
return this.discounts.filter(disc => {
let res = disc.condition(this.orders);
// console.log(res);
return res;
});
}
},
methods: {
remove(id) {
this.$emit('delete-order', id);
},
updateQuantity(itemId, v) {
// console.log(itemId, v);
this.$emit('update-quant-cart', { id: itemId, quantity: parseInt(v, 10) });
}
},
filters: {
money(v) {
if (typeof v === 'undefined' || v === null) {
return (0).toFixed(2);
}
return v.toFixed(2);
}
}
};
// --- MAIN COMPONENT -----------------------
let app = new Vue({
el: '#app',
data: {
loading: false,
items: [],
discounts: [],
orders: []
},
components: {
'app-item-selector': ItemSelector,
'app-order-display': OrderDisplay
},
created() {
this.loading = true;
Promise.all([
apiService.getItems(),
apiService.getDiscounts()
]).then(resps => {
console.log(resps);
this.loading = false;
['items', 'discounts'].map((el, i) => {
this[el] = resps[i];
});
}).catch(err => {
console.error(err);
this.loading = false;
});
},
methods: {
addItem(itemId) {
// console.log('add item', itemId);
let item = this.items.find(item => item.id === itemId);
let order = this.orders.find(item => item.id === itemId);
if (typeof order === 'undefined') {
this.orders.push(item);
} else {
order.quantity++;
}
},
deleteOrder(id) {
let i = this.orders.find(o => o.id === id);
this.orders.splice(i, 1);
},
updateQuantity(o) {
let item = this.orders.find(item => item.id === o.id);
item.quantity = o.quantity;
}
}
});
}());
@rebolyte
Copy link
Author

rebolyte commented Oct 2, 2017

@rebolyte
Copy link
Author

Original exercise prompt:

The original Star Wars movies have been re-released on DVD and Blu-Ray. Your friend Piratical Liz, who has a boat off the coast in international waters, thinks she can undersell the distributor with cheap knock-off discs. She needs to build an online store to sell the dodgy movies. You are her only friend except for a crab named Gerald, and Gerald's strong sense of ethics has prevented him from taking part in this tomfoolery. The task of building the online store will have to fall to you.

Build a basic web application that implements a shopping cart. Piratical Liz's store will need to stock the following items:

  • Star Wars Episode IV DVD ($20)
  • Star Wars Episode V DVD ($20)
  • Star Wars Episode VI DVD ($20)
  • Star Wars Episode IV Blu-Ray ($25)
  • Star Wars Episode V Blu-Ray ($25)
  • Star Wars Episode VI Blu-Ray ($25)

When you add an item to the shopping cart, the cart should indicate the total number of items and the total cost. Customers should be able to change the quantity of each item after it has been added. Customers should also be able to remove an item from the cart entirely.

If a customer adds all the different DVDs to their cart, they will automatically receive a 10% discount on those items only. If the customer adds all the Blu-Rays to their cart, they will automatically receive a 15% discount on those items only. These discounts should be indicated so the customer can see their savings.

Good luck, and don't get caught!

Stretch Goal

Implement a bulk discount, so that if the customer adds 100 items or more to their cart, they will receive a 5% discount on their total. This discount should be applied to the total after the discounts above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment