fix(image): New Image component to consistently handle broken images

- psuedo element hack didn't work cross browser
diff --git a/erpnext/public/js/hub/components/DetailView.vue b/erpnext/public/js/hub/components/DetailView.vue
index 2f1a941..cc09982 100644
--- a/erpnext/public/js/hub/components/DetailView.vue
+++ b/erpnext/public/js/hub/components/DetailView.vue
@@ -28,7 +28,7 @@
 			<div class="row margin-bottom">
 				<div class="col-md-3">
 					<div class="hub-item-image">
-						<img v-img-src="image">
+						<base-image :src="image" :alt="title" />
 					</div>
 				</div>
 				<div class="col-md-8">
diff --git a/erpnext/public/js/hub/components/Image.vue b/erpnext/public/js/hub/components/Image.vue
new file mode 100644
index 0000000..9acf421
--- /dev/null
+++ b/erpnext/public/js/hub/components/Image.vue
@@ -0,0 +1,40 @@
+<template>
+	<div class="hub-image">
+		<img :src="src" :alt="alt" v-show="!is_loading && !is_broken"/>
+		<div class="hub-image-loading" v-if="is_loading">
+			<span class="octicon octicon-cloud-download"></span>
+		</div>
+		<div class="hub-image-broken" v-if="is_broken">
+			<span class="octicon octicon-file-media"></span>
+		</div>
+	</div>
+</template>
+<script>
+export default {
+	name: 'Image',
+	props: ['src', 'alt'],
+	data() {
+		return {
+			is_loading: true,
+			is_broken: false
+		}
+	},
+	created() {
+		this.handle_image();
+	},
+	methods: {
+		handle_image() {
+			let img = new Image();
+			img.src = this.src;
+
+			img.onload = () => {
+				this.is_loading = false;
+			};
+			img.onerror = () => {
+				this.is_loading = false;
+				this.is_broken = true;
+			};
+		}
+	}
+};
+</script>
diff --git a/erpnext/public/js/hub/components/ItemCard.vue b/erpnext/public/js/hub/components/ItemCard.vue
index f34fddc..675ad86 100644
--- a/erpnext/public/js/hub/components/ItemCard.vue
+++ b/erpnext/public/js/hub/components/ItemCard.vue
@@ -15,7 +15,7 @@
 				</i>
 			</div>
 			<div class="hub-card-body">
-				<img class="hub-card-image" v-img-src="item.image"/>
+				<base-image class="hub-card-image" :src="item.image" :alt="title" />
 				<div class="hub-card-overlay">
 					<div v-if="is_local" class="hub-card-overlay-body">
 						<div class="hub-card-overlay-button">
diff --git a/erpnext/public/js/hub/components/ItemListCard.vue b/erpnext/public/js/hub/components/ItemListCard.vue
index 70cb566..7f6fb77 100644
--- a/erpnext/public/js/hub/components/ItemListCard.vue
+++ b/erpnext/public/js/hub/components/ItemListCard.vue
@@ -1,7 +1,7 @@
 <template>
 	<div class="hub-list-item" :data-route="item.route">
 		<div class="hub-list-left">
-			<img class="hub-list-image" v-img-src="item.image">
+			<base-image class="hub-list-image" :src="item.image" />
 			<div class="hub-list-body ellipsis">
 				<div class="hub-list-title">{{item.item_name}}</div>
 				<div class="hub-list-subtitle ellipsis">
diff --git a/erpnext/public/js/hub/vue-plugins.js b/erpnext/public/js/hub/vue-plugins.js
index 439c1f2..6e6a7cb 100644
--- a/erpnext/public/js/hub/vue-plugins.js
+++ b/erpnext/public/js/hub/vue-plugins.js
@@ -7,6 +7,7 @@
 import DetailView from './components/DetailView.vue';
 import DetailHeaderItem from './components/DetailHeaderItem.vue';
 import EmptyState from './components/EmptyState.vue';
+import Image from './components/Image.vue';
 
 Vue.prototype.__ = window.__;
 Vue.prototype.frappe = window.frappe;
@@ -17,6 +18,7 @@
 Vue.component('detail-view', DetailView);
 Vue.component('detail-header-item', DetailHeaderItem);
 Vue.component('empty-state', EmptyState);
+Vue.component('base-image', Image);
 
 Vue.directive('route', {
 	bind(el, binding) {
@@ -51,16 +53,6 @@
 	img.src = src;
 }
 
-Vue.directive('img-src', {
-	bind(el, binding) {
-		handleImage(el, binding.value);
-	},
-	update(el, binding) {
-		if (binding.value === binding.oldValue) return;
-		handleImage(el, binding.value);
-	}
-});
-
 Vue.filter('striphtml', function (text) {
 	return strip_html(text || '');
 });
\ No newline at end of file
diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less
index d40926b..089915d 100644
--- a/erpnext/public/less/hub.less
+++ b/erpnext/public/less/hub.less
@@ -1,4 +1,5 @@
-@import "../../../../frappe/frappe/public/less/variables.less";
+@import "variables.less";
+@import (reference) "desk.less";
 
 body[data-route^="marketplace/"] {
 	.layout-side-section {
@@ -26,6 +27,22 @@
 		font-size: @text-medium;
 	}
 
+	.hub-image {
+		height: 200px;
+	}
+
+	.hub-image-loading, .hub-image-broken {
+		.img-background();
+		display: flex;
+		align-items: center;
+		justify-content: center;
+
+		span {
+			font-size: 32px;
+			color: @text-extra-muted;
+		}
+	}
+
 	.progress-bar {
 		background-color: #89da28;
 	}
@@ -136,6 +153,7 @@
 	}
 
 	.hub-item-image {
+		position: relative;
 		border: 1px solid @border-color;
 		border-radius: 4px;
 		overflow: hidden;