Skip to content

Instantly share code, notes, and snippets.

@eiriklv
Created May 27, 2017 10:30
Show Gist options
  • Save eiriklv/9e843af9f8543bfc7d5dfaccc2323863 to your computer and use it in GitHub Desktop.
Save eiriklv/9e843af9f8543bfc7d5dfaccc2323863 to your computer and use it in GitHub Desktop.
Data modeling an online store
/**
* Modeling an online store with categories, sub-categories and products
*
* NOTE: The important thing is to be able to
* describe any requirement with the chosen data model
*
* NOTE: Another important thing is conventional interfaces
* and impedance matching. Data should just flow through the
* functions without needing to conform to many different interfaces
*/
/**
* Product model
*/
const product = {
type: 'phone',
id: 'nokia-3310',
name: 'Nokia 3310',
description: 'Most amazing phone.....',
manufacturer: 'nokia',
categories: ['phone'],
price: 399,
inventory: 2,
specifications: {
storageSize: 8,
screenSize: 4.3,
},
};
/**
* Alternative product model (with color variants)
*/
const productWithVariants = {
type: 'phone-with-color-variants',
id: 'samsung-s7-edge',
name: 'Samsung S7 Edge',
description: 'Amazing phone.....',
manufacturer: 'samsung',
categories: ['phone'],
variants: [{
id: 'samsung-s7-edge-black',
color: 'black',
inventory: 5,
price: 799,
}, {
id: 'samsung-s7-edge-gold',
color: 'white',
inventory: 7,
price: 899,
}],
specifications: {
storageSize: 32,
screenSize: 7.9,
},
};
/**
* Alternative product model (with storage and color variants and discount option)
*/
const productWithStorageAndColorVariants = {
type: 'phone-with-storage-and-color-variants',
id: 'iphone-6s',
name: 'iPhone 6S',
description: 'Amazing phone.....',
manufacturer: 'apple',
categories: ['phone'],
storageVariants: [{
id: 'iphone-6s-64gb',
size: 64,
price: 1099,
discount: -200,
colorVariants: [{
id: 'iphone-6s-64gb-black',
color: 'black',
inventory: 5,
}, {
id: 'iphone-6s-64gb-white',
color: 'white',
inventory: 7,
}]
}, {
id: 'iphone-6s-128gb',
size: 128,
price: 1299,
colorVariants: [{
id: 'iphone-6s-128gb-black',
color: 'black',
inventory: 3,
}, {
id: 'iphone-6s-128gb-white',
color: 'white',
inventory: 9,
}]
}],
};
/**
* Manufacturer model
*/
const manufacturer = {
id: 'apple',
name: 'Apple',
};
/**
* Category model
*/
const category = {
id: 'phone',
name: 'Phones',
description: 'Things you never use to call someone with..',
};
/**
* Example product list
*/
const products = [{
type: 'phone-with-storage-and-color-variants',
id: 'iphone-6s',
name: 'iPhone 6S',
description: 'Amazing phone.....',
manufacturer: 'apple',
categories: ['phone'],
storageVariants: [{
id: 'iphone-6s-64gb',
size: 64,
price: 1099,
colorVariants: [{
id: 'iphone-6s-64gb-black',
color: 'black',
inventory: 5,
}, {
id: 'iphone-6s-64gb-white',
color: 'white',
inventory: 7,
}]
}, {
id: 'iphone-6s-128gb',
size: 128,
price: 1299,
colorVariants: [{
id: 'iphone-6s-128gb-black',
color: 'black',
inventory: 3,
}, {
id: 'iphone-6s-128gb-white',
color: 'white',
inventory: 9,
}]
}],
}, {
type: 'phone-with-color-variants',
id: 'samsung-s7-edge',
name: 'Samsung S7 Edge',
description: 'Amazing phone.....',
manufacturer: 'samsung',
categories: ['phone'],
variants: [{
id: 'samsung-s7-edge-black',
color: 'black',
inventory: 5,
price: 799,
}, {
id: 'samsung-s7-edge-gold',
color: 'white',
inventory: 7,
price: 899,
}],
specifications: {
storageSize: 32,
screenSize: 7.9,
},
}, {
type: 'phone',
id: 'nokia-3310',
name: 'Nokia 3310',
description: 'Most amazing phone.....',
manufacturer: 'nokia',
categories: ['phone'],
price: 399,
inventory: 2,
specifications: {
storageSize: 8,
screenSize: 4.3,
},
}, {
type: 'tablet',
id: 'ipad-pro-12',
name: 'iPad Pro 12',
description: 'Amazing tablet.....',
manufacturer: 'apple',
categories: ['tablet'],
price: 1299,
inventory: 5,
specifications: {
storageSize: 128,
screenSize: 12.9
},
}, {
type: 'tablet',
id: 'samsung-tab-a',
name: 'Samsung Tab A',
description: 'Other amazing tablet.....',
manufacturer: 'samsung',
categories: ['tablet'],
price: 1099,
inventory: 15,
specifications: {
storageSize: 64,
},
}, {
type: 'cable',
id: 'lightning-cable-3m',
name: 'Lightning cable (3m)',
description: 'Amazing cable.....',
manufacturer: 'apple',
categories: ['cable'],
price: 699,
inventory: 10,
specifications: {
length: 3,
},
}, {
type: 'cable',
id: 'usb-c-cable-5m',
name: 'USB-C cable (5m)',
description: 'Other amazing cable.....',
manufacturer: 'samsung',
categories: ['cable'],
price: 8,
inventory: 50,
specifications: {
length: 5,
},
}, {
type: 'protector',
id: 'screen-protector-iphone',
name: 'iPhone Screen Protector',
description: 'Protect your screen with.....',
manufacturer: 'apple',
categories: ['protector'],
price: 20,
inventory: 50,
specifications: {
length: 5,
},
}];
/**
* Example manufacturer list
*/
const manufacturers = [{
id: 'apple',
name: 'Apple',
}, {
id: 'samsung',
name: 'Samsung',
}, {
id: 'nokia',
name: 'Nokia',
}];
/**
* Example category list
*/
const categories = [{
id: 'device',
name: 'Devices',
description: 'Blablabla..'
}, {
id: 'phone',
name: 'Phones',
description: 'Blablabla..'
}, {
id: 'tablet',
name: 'Tablets',
description: 'Blablabla..'
}, {
id: 'accessory',
name: 'Accessories',
description: 'Blablabla..'
}, {
id: 'charger',
name: 'Chargers',
description: 'Blablabla..'
}, {
id: 'protector',
name: 'Protectors',
description: 'Blablabla..'
}];
/**
* Example category hierarchy
*
* How many levels?
*/
const categoryHierarchy = [{
id: 'device',
children: [
'phone',
'tablet',
],
}, {
id: 'accessory',
children: [
'charger',
'protection',
],
}];
/**
* Example category hierarchy (alternative)
*/
const frontPagePromotionCategories = [{
name: 'Phones'
categories: ['phone']
limit: 4,
}, {
name: 'Tablets',
categories: ['tablet']
limit: 4,
}, {
name: 'Accessories',
categories: ['charger', 'protector'],
limit: 8
}];
/**
* Example category hierarchy (alternative)
*/
const frontPagePromotionProducts = [{
name: 'Phones'
products: ['nokia-3310', 'iphone-6s', 'samsung-s7-edge'],
}, {
name: 'Tablets',
products: ['ipad-pro-12']
}, {
name: 'Accessories',
products: ['usb-c-cable-5m', 'screen-protector-iphone'],
}];
/**
* Cart item model
*/
const cartItem = { productId: 'nokia-3310' };
/**
* Cart item model (alternative)
*/
const cartItem = {
productId: 'samsung-s7-edge',
variantId: 'samsung-s7-edge-gold',
};
/**
* Cart item model (alternative)
*/
const cartItem = {
productId: 'iphone-6s',
storageVariantId: 'iphone-6s-64gb',
colorVariantId: 'iphone-6s-64gb-black',
};
/**
* Example cart list
*/
const cart = [
{ productId: 'nokia-3310' },
{ productId: 'samsung-s7-edge', variantId: 'samsung-s7-edge-gold' },
{ productId: 'iphone-6s', storageVariantId: 'iphone-6s-64gb', colorVariantId: 'iphone-6s-64gb-black' },
];
/**
* General utility
*/
function getById(collection, id) {
return collection.find((item) => item.id === id);
}
/**
* Product selector
*/
function getProductById(productId) {
return getById(products, productId);
}
/**
* Category selector
*/
function getCategoryById(categoryId) {
return getById(categories, categoryId);
}
/**
* Manufacturer selector
*/
function getManufacturerById(manufacturerId) {
return getById(manufacturers, manufacturerId);
}
/**
* Cart item price selector
*/
function getCartItemPrice(cartItem) {
const priceGetters = {
'phone': ({ productId }) => getById(productId).price,
'phone-with-color-variants': ({ productId, variantId }) => getById(getById(productId).variants, variantId).price,
'phone-with-storage-and-color-variants': ({ productId, storageVariantId, colorVariantId }) => {
const selectedVariant = getById(getById(getById(productId).storageVariants, storageVariantId).colorVariants, colorVariantId);
const { price = 0, discount = 0 } = selectedVariant;
return price + discount;
},
default: () => {
console.error('Unsupported product type');
return 0;
},
};
const product = getProductById(cartItem.productId);
return (priceGetters[product.type] || priceGetters.default)(cartItem);
}
/**
* Cart item price selector
*/
function getListRepresentationOfProduct(product) {
const informationGetters = {
'phone': ({ name, description, price }) => ({ name, description, price: [price] }),
'phone-with-color-variants': ({ name, description, colorVariants }) => ({ name, description, price: colorVariants.map(({ price }) => price)}),
'phone-with-storage-and-color-variants': ({ productId, storageVariantId, colorVariantId }) => {
const selectedVariant = getById(getById(productId).storageVariants, storageVariantId).price;
const { price = 0, discount = 0 } = selectedVariant;
return price + discount;
},
default: () => {
console.error('Unsupported product type');
return 0;
},
};
return (informationGetters[product.type] || informationGetters.default)(cartItem);
}
[
{ name, description, price: [100] },
{ name, description, price: [100, 500] }
]
function sum(a, b) {
return a + b;
}
/**
* Cart total selector
*/
function getCartTotalPrice(cart) {
return cart
.map(getCartItemPrice)
.reduce(sum, 0);
}
/**
* Getting the prices for each item in the cart
*/
const prices = cart.map(getCartItemPrice);
// [399, 899, 899]
/**
* Getting the total sum of the cart
*/
const total = getCartTotalPrice(cart);
// 2197
/**
* Listing out products from the category hierarchy
* (map + filter + reduce)
*
* NOTE: Use the url/query as state
*
* /category/:activeCategoryId/:activeSubCategoryId
*
* /product/:productId
*/
/**
* General utility
*/
function getByCategory(collection, categories) {
return collection.filter((item) => {
return item.categories.some((category) => categories.includes(category))
});
}
/**
* Product selector (by category)
*/
function getProductsByCategoryId(categories) {
return getByCategory(products, categories);
}
/**
* Set the active (top level) category
*/
const activeCategoryId = 'accessory';
/**
* Get the data of the active (top level) category
*/
const activeCategory = getCategoryById(activeCategoryId);
// { name, description }
/**
* Get the applicable sub-categories
*/
const subCategoryIds = activeCategory.children;
// ['charger', 'protector']
/**
* Set the active sub-category
*/
const activeSubCategoryId = 'charger';
/**
* Get the data of the active sub-category
*/
const activeSubCategory = getCategoryById(activeSubCategoryId);
// { name, description }
/**
* Get the currently active products (that we want to list)
* based on the category level chosen
*/
const activeProducts = activeSubCategoryId ?
getProductsByCategory([activeSubCategoryId]) :
getProductsByCategory(subCategoryIds);
/**
* Getting filter options
* (map + reduce)
*/
function uniqueReducer(list, item) {
return [
...list,
...(list.includes(item) ? [] : [list]),
];
}
const manufacturerOptions = activeProducts
.map(({ manufacturer }) => manufacturer)
.reduce(uniqueReducer, [])
// ['apple', 'samsung']
.map(getManufacturerById)
// [{ ... }, { ... }]
/**
* Set filter values
*/
const filters = {
manufacturers: ['apple', 'samsung'],
}
/**
* Get the currently visible products based on filtering
*/
const visibleProducts = activeProducts
.filter(({ manufacturer }) => filters.manufacturers.includes(manufacturer))
/**
* Example state
*
* NOTE: Describing your entire application with data
*/
const state = {
categoryHierarchy: [...],
frontPagePromotionCategories: [...],
frontPagePromotionProducts: [...],
categories: [...],
manufacturers: [...],
products: [...],
filters: {...},
cart: [],
activeCategoryId: '...', // or part of url/query
activeSubCategoryId: '...', // or part of url/query
activeProductId: '...', // or part of url/query
};
/**
* Component types
*/
class Phone extends React.Component {
handleAddToCart() {
const { addToCart, productId } = this.props;
addToCart({ productId });
}
render() {
// Handle rendering of a simple phone product
}
}
class PhoneWithColorVariants extends React.Component {
constructor(props) {
super(props);
this.state = {
activeVariantId: '',
};
}
handleAddToCart() {
const { addToCart, productId } = this.props;
const { activeVariantId } = this.state;
addToCart({
productId,
variantId: activeVariantId
});
}
render() {
// Handle rendering of a phone product with color variants
}
}
class PhoneWithStorageAndColorVariants extends React.Component {
constructor(props) {
super(props);
this.state = {
activeStorageVariantId: '',
activeColorVariantId: '',
};
}
// /products/iphone-6s?storageVariant=64gb&colorVariant=black
handleAddToCart() {
const { addToCart, params: { productId } } = this.props;
const { activeStorageVariantId, activeColorVariantId } = this.state;
addToCart({
productId,
storageVariantId: activeStorageVariantId,
colorVariantId: activeColorVariantId,
});
}
render() {
// Handle rendering of a phone product with storage and color variants
}
}
/**
* Mapping product types into component/screen types
* (mapping + selectors)
*/
function getProductComponentByType({ type }) {
const productComponents = {
'phone': Phone,
'phone-with-color-variants': PhoneWithColorVariants,
'phone-with-storage-and-color-variants': PhoneWithStorageAndColorVariants,
'tablet': Tablet,
'cable': Cable,
'protector': Accessory,
default: NullComponent,
};
return productComponents[type] || productComponents.default;
}
/**
* Mapping the entire state into an app with categories, sub-categories, products and a cart
*/
const visibleProducts = [...];
const visibleProductElements = visibleProducts
.map(getProductComponentByType)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment