Faceted Search

by ninty9notout

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<div class="spinner">
    <div class="bounce1"></div>
    <div class="bounce2"></div>
    <div class="bounce3"></div>
</div>

<div class="results results--hidden">
	<div class="filters">
		<div class="filters_inner">
			<!-- intentionally empty -->
		</div>
	</div>

	<div class="content">
		<div class="content__inner">

		</div>
	</div>
</div>

<script type="text/html" id="facet-group">
	<div class="facet-group">
		<h3 class="facet-group__title"><%= title %></h3>

		<div class="facet-list">
			<% _.forEach(options, (option) => { %>
				<label class="facet-item">
					<input type="checkbox" class="checkbox checkbox-facet" name="<%= title %>" value="<%= option.title %>">

					<span class="label"><%= option.title %></span>
				</label>
			<% }) %>
		</div>
	</div>
</script>

<script type="text/html" id="result-item">
	<div class="result-item">
		<h2 class="result-item__title"><strong><%= make %>:</strong> <%= model %></h2>
	</div>
</script>

CSS

html {
	box-sizing: border-box;
}

*, *:before, *:after {
	box-sizing: inherit;
}

html, body {
	width: 100%;
	height: 100%;
	margin: 0;
	padding: 0;
}

body {
	background-color: #34495E;
	font-family: "Lato", Helvetica, Arial, sans-serif;
	font-size: 16px;
	line-height: 1.5;
	color: #ECF0F1;
}

.spinner {
    margin: 100px auto 0;
    width: 70px;
    text-align: center;
}

.spinner--hidden {
	display: none;
}

.spinner > div {
    width: 18px;
    height: 18px;
    background-color: #ECF0F1;

    border-radius: 100%;
    display: inline-block;
    animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}

.spinner .bounce1 {
    animation-delay: -0.32s;
}

.spinner .bounce2 {
    animation-delay: -0.16s;
}


@keyframes sk-bouncedelay {
    0%, 80%, 100% { 
        transform: scale(0);
    }

    40% {
        transform: scale(1.0);
    }
}

.results {
	display: flex;
	min-height: 100vh;
}

.results--hidden {
	display: none;
}

.filters {
	flex: 1 0 auto;
	max-width: 175px;
	padding-left: 1em;
}

.content {
	flex: 1 0 auto;
}

.facet-group {
	padding: 0.5em;
}

.facet-group + .facet-group {
	margin-top: 0.75em;
	padding-top: 0.75em;
	border-top: 1px dashed rgba(255, 255, 255, 0.15);
}

.facet-group__title {
	margin: 0 0 0.25em;
}

.facet-list {
	max-height: 144px;
	overflow-y: auto;
	background-image: radial-gradient(ellipse farthest-corner at center top, rgba(0, 0, 0, 0.15) 0, transparent 75%), radial-gradient(ellipse farthest-corner at center bottom, rgba(0, 0, 0, 0.15) 0, transparent 75%);
    background-size: 100% 10px;
    background-repeat: no-repeat;
    background-attachment: scroll;
    background-position: top, bottom;
}

.facet-item:first-child,
.facet-item:last-child {
	background-size: 100% 100%;
	background-repeat: no-repeat;
	background-attachment: scroll;
}

.facet-item:first-child {
	background-image: linear-gradient(180deg, #34495E 50%, hsla(0, 0%, 94%, 0));
	background-position: top;
}

.facet-item:last-child {
	background-image:...

JavaScript

const map = _.map;
const forEach = _.forEach;
const isFunction = _.isFunction;
const isEqual = _.isEqual;
const clone = _.clone;
const get = _.get;
const has = _.has;
const values = _.values;
const template = _.template;
const reject = _.reject;
const intersectionWith = _.intersectionWith;

const SELECTORS = {
	LOADER: '.spinner',
	RESULTS: '.results',
	FILTER_INNER: '.filters_inner',
	CONTENT_INNER: '.content__inner',
};

const CLASSES = {
	LOADER_HIDDEN: 'spinner--hidden',
	RESULTS_HIDDEN: 'results--hidden',
};

const ELEMENTS = {
	LOADER: document.querySelector(SELECTORS.LOADER),
	RESULTS: document.querySelector(SELECTORS.RESULTS),
	FILTER_INNER: document.querySelector(SELECTORS.FILTER_INNER),
	CONTENT_INNER: document.querySelector(SELECTORS.CONTENT_INNER),
};

const TEMPLATES = {
	FILTER_GROUP: template( document.querySelector('#facet-group').innerHTML ),
	RESULT_ITEM: template( document.querySelector('#result-item').innerHTML ),
};

const facet_keys = {
	make: 'Make',
	model: 'Model',
	gender: 'Gender',
	state: 'State',
};

const getData = () => fetch('https://api.myjson.com/bins/1h96xq').then(response => {
	return response.json();
});

const getProcessedData = () => {
	return getData().then((data) => {
		return data.map((item) => {
			item.facets = map(facet_keys, (title, key) => {
				return {
					group: title,
					option: item[key],
				};
			});

			return item;
		});
	});
}

const generateFacets = data => {
	return map(facet_keys, (title, key) => {
		let options = data.map(item => {
			return get(item, key, null);
		});	

		options = options.filter(item => {
			return item !== null;
		});

		options = options.reduce((carry, item) => {
			if (has(carry, item) === false) {
				carry[item] = {
					title: item,
					count: 0,
				};
			}

			carry[item].count++;

			return carry;
		}, {});
		
		return {
			title: title,
			key: key,
			options: values(options),
		};
	});
};

let loading = true;
let facets = [];
let results = [];
let appliedFacets =...