Merge branch 'hub-redesign' of https://github.com/frappe/erpnext into hub-redesign
diff --git a/erpnext/public/js/hub/PageContainer.vue b/erpnext/public/js/hub/PageContainer.vue
index 456da9a..ca4c63a 100644
--- a/erpnext/public/js/hub/PageContainer.vue
+++ b/erpnext/public/js/hub/PageContainer.vue
@@ -10,11 +10,18 @@
 import Category from './pages/Category.vue';
 import Search from './pages/Search.vue';
 import PublishedProducts from './pages/PublishedProducts.vue';
+import Buying from './pages/Buying.vue';
+import BuyingMessages from './pages/BuyingMessages.vue';
 
 const route_map = {
 	'marketplace/home': Home,
 	'marketplace/saved-products': SavedProducts,
-	'marketplace/publish': Publish
+	'marketplace/my-products': PublishedProducts,
+	'marketplace/publish': Publish,
+	'marketplace/category/:category': Category,
+	'marketplace/search/:keyword': Search,
+	'marketplace/buying': Buying,
+	'marketplace/buying/:item': BuyingMessages
 }
 
 export default {
@@ -33,7 +40,41 @@
 			this.current_page = this.get_current_page();
 		},
 		get_current_page() {
-			return route_map[frappe.get_route_str()];
+			const curr_route = frappe.get_route_str();
+			let route = Object.keys(route_map).filter(route => route == curr_route)[0];
+
+			if (!route) {
+				// find route by matching it with dynamic part
+				const curr_route_parts = curr_route.split('/');
+				const weighted_routes = Object.keys(route_map)
+					.map(route_str => route_str.split('/'))
+					.filter(route_parts => route_parts.length === curr_route_parts.length)
+					.reduce((obj, route_parts) => {
+						const key = route_parts.join('/');
+						let weight = 0;
+						route_parts.forEach((part, i) => {
+							const curr_route_part = curr_route_parts[i];
+							if (part === curr_route_part || curr_route_part.includes(':')) {
+								weight += 1;
+							}
+						});
+
+						obj[key] = weight;
+						return obj;
+					}, {});
+
+				// get the route with the highest weight
+				let weight = 0
+				for (let key in weighted_routes) {
+					const route_weight = weighted_routes[key];
+					if (route_weight > weight) {
+						route = key;
+						weight = route_weight;
+					}
+				}
+			}
+
+			return route_map[route];
 		}
 	}
 }
diff --git a/erpnext/public/js/hub/components/CommentInput.vue b/erpnext/public/js/hub/components/CommentInput.vue
new file mode 100644
index 0000000..cc430d0
--- /dev/null
+++ b/erpnext/public/js/hub/components/CommentInput.vue
@@ -0,0 +1,38 @@
+<template>
+	<div>
+		<div ref="comment-input"></div>
+		<div class="level">
+			<div class="level-left">
+				<span class="text-muted">{{ __('Ctrl + Enter to submit') }}</span>
+			</div>
+			<div class="level-right">
+				<button class="btn btn-primary btn-xs" @click="submit_input">{{ __('Submit') }}</button>
+			</div>
+		</div>
+	</div>
+</template>
+<script>
+export default {
+	mounted() {
+		this.make_input();
+	},
+	methods: {
+		make_input() {
+			this.message_input = new frappe.ui.CommentArea({
+				parent: this.$refs['comment-input'],
+				on_submit: (message) => {
+					this.message_input.reset();
+					this.$emit('change', message);
+				},
+				no_wrapper: true
+			});
+		},
+		submit_input() {
+			if (!this.message_input) return;
+			const value = this.message_input.val();
+			if (!value) return;
+			this.message_input.submit();
+		}
+	}
+}
+</script>
diff --git a/erpnext/public/js/hub/components/ItemCard.vue b/erpnext/public/js/hub/components/ItemCard.vue
index 7975868..f34fddc 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" :src="item.image"/>
+				<img class="hub-card-image" v-img-src="item.image"/>
 				<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/ItemCardsContainer.vue b/erpnext/public/js/hub/components/ItemCardsContainer.vue
index 5af82de..748c272 100644
--- a/erpnext/public/js/hub/components/ItemCardsContainer.vue
+++ b/erpnext/public/js/hub/components/ItemCardsContainer.vue
@@ -5,8 +5,7 @@
 			:message="empty_state_message"
 			:bordered="true"
 			:height="80"
-		>
-		</empty-state>
+		/>
 		<item-card
 			v-for="item in items"
 			:key="container_name + '_' +item[item_id_fieldname]"
diff --git a/erpnext/public/js/hub/components/ItemListCard.vue b/erpnext/public/js/hub/components/ItemListCard.vue
new file mode 100644
index 0000000..4c68100
--- /dev/null
+++ b/erpnext/public/js/hub/components/ItemListCard.vue
@@ -0,0 +1,23 @@
+<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">
+			<div class="hub-list-body ellipsis">
+				<div class="hub-list-title">{{item.item_name}}</div>
+				<div class="hub-list-subtitle ellipsis">
+					<span>{{message.sender}}: </span>
+					<span>{{message.content}}</span>
+
+				</div>
+			</div>
+		</div>
+		<div class="hub-list-right">
+			<span class="text-muted" v-html="frappe.datetime.comment_when(message.creation, true)" />
+		</div>
+	</div>
+</template>
+<script>
+export default {
+	props: ['item', 'message']
+}
+</script>
diff --git a/erpnext/public/js/hub/components/SearchInput.vue b/erpnext/public/js/hub/components/SearchInput.vue
index 6647b15..4b1ce6e 100644
--- a/erpnext/public/js/hub/components/SearchInput.vue
+++ b/erpnext/public/js/hub/components/SearchInput.vue
@@ -5,7 +5,7 @@
 		class="form-control"
 		:placeholder="placeholder"
 		:value="value"
-		@input="on_input">
+		@keydown.enter="on_input">
   </div>
 </template>
 
@@ -20,13 +20,7 @@
 		on_input(event) {
 			this.$emit('input', event.target.value);
 			this.on_search();
-
-			// TODO: Debouncing doesn't fire search
-			// frappe.utils.debounce(this.on_search, 500);
 		}
 	}
 };
 </script>
-
-<style lang="scss" scoped>
-</style>
diff --git a/erpnext/public/js/hub/components/SectionHeader.vue b/erpnext/public/js/hub/components/SectionHeader.vue
new file mode 100644
index 0000000..05a2f83
--- /dev/null
+++ b/erpnext/public/js/hub/components/SectionHeader.vue
@@ -0,0 +1,3 @@
+<template>
+    <div class="hub-items-header level"><slot></slot></div>
+</template>
diff --git a/erpnext/public/js/hub/marketplace.js b/erpnext/public/js/hub/marketplace.js
index 00b6aee..10a55aa 100644
--- a/erpnext/public/js/hub/marketplace.js
+++ b/erpnext/public/js/hub/marketplace.js
@@ -1,4 +1,5 @@
 import Vue from 'vue/dist/vue.js';
+import './vue-plugins';
 
 // pages
 import './pages/item';
@@ -148,12 +149,12 @@
 
 	make_body() {
 		this.$body = this.$parent.find('.layout-main-section');
-		// this.$page_container = $('<div class="hub-page-container">').appendTo(this.$body);
+		this.$page_container = $('<div class="hub-page-container">').appendTo(this.$body);
 
-		// new Vue({
-		// 	el: '.hub-page-container',
-		// 	render: h => h(PageContainer)
-		// });
+		new Vue({
+			el: '.hub-page-container',
+			render: h => h(PageContainer)
+		});
 
 		erpnext.hub.on('seller-registered', () => {
 			this.registered = 1;
@@ -174,6 +175,10 @@
 	}
 
 	refresh() {
+
+	}
+
+	_refresh() {
 		const route = frappe.get_route();
 		this.subpages = this.subpages || {};
 
diff --git a/erpnext/public/js/hub/pages/Buying.vue b/erpnext/public/js/hub/pages/Buying.vue
new file mode 100644
index 0000000..7a783bc
--- /dev/null
+++ b/erpnext/public/js/hub/pages/Buying.vue
@@ -0,0 +1,46 @@
+<template>
+	<div>
+		<section-header>
+			<h4>{{ __('Buying') }}</h4>
+		</section-header>
+		<div class="row">
+			<div class="col-md-7"
+				v-for="item of items"
+				:key="item.name"
+			>
+				<item-list-card
+					:item="item"
+					:message="item.recent_message"
+					v-route="'marketplace/buying/' + item.name"
+				/>
+			</div>
+		</div>
+	</div>
+</template>
+<script>
+import SectionHeader from '../components/SectionHeader.vue';
+import ItemListCard from '../components/ItemListCard.vue';
+
+export default {
+	components: {
+		SectionHeader,
+		ItemListCard
+	},
+	data() {
+		return {
+			items: []
+		}
+	},
+	created() {
+		this.get_items_for_messages()
+			.then(items => {
+				this.items = items;
+			});
+	},
+	methods: {
+		get_items_for_messages() {
+			return hub.call('get_buying_items_for_messages', {}, 'action:send_message');
+		}
+	}
+}
+</script>
diff --git a/erpnext/public/js/hub/pages/BuyingMessages.vue b/erpnext/public/js/hub/pages/BuyingMessages.vue
new file mode 100644
index 0000000..c718647
--- /dev/null
+++ b/erpnext/public/js/hub/pages/BuyingMessages.vue
@@ -0,0 +1,88 @@
+<template>
+	<div v-if="item_details">
+
+		<section-header>
+			<div class="flex flex-column margin-bottom">
+				<h4>{{ item_details.item_name }}</h4>
+				<span class="text-muted">{{ item_details.company }}</span>
+			</div>
+		</section-header>
+		<div class="row">
+			<div class="col-md-7">
+				<div class="message-container">
+					<div class="message-list">
+						<div class="level margin-bottom" v-for="message in messages" :key="message.name">
+							<div class="level-left ellipsis" style="width: 80%;">
+								<div v-html="frappe.avatar(message.sender)" />
+								<div style="white-space: normal;" v-html="message.content" />
+							</div>
+							<div class="level-right text-muted" v-html="frappe.datetime.comment_when(message.creation, true)" />
+						</div>
+					</div>
+					<div class="message-input">
+						<comment-input @change="send_message" />
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</template>
+<script>
+import SectionHeader from '../components/SectionHeader.vue';
+import CommentInput from '../components/CommentInput.vue';
+import ItemListCard from '../components/ItemListCard.vue';
+
+export default {
+	components: {
+		SectionHeader,
+		CommentInput,
+		ItemListCard
+	},
+	data() {
+		return {
+			item_details: null,
+			messages: []
+		}
+	},
+	created() {
+		const hub_item_code = this.get_hub_item_code();
+		this.get_item_details(hub_item_code)
+			.then(item_details => {
+				this.item_details = item_details;
+				this.get_messages(item_details)
+					.then(messages => {
+						this.messages = messages;
+					});
+			});
+	},
+	methods: {
+		send_message(message) {
+			this.messages.push({
+				sender: hub.settings.company_email,
+				content: message,
+				creation: Date.now(),
+				name: frappe.utils.get_random(6)
+			});
+			hub.call('send_message', {
+				from_seller: hub.settings.company_email,
+				to_seller: this.item_details.hub_seller,
+				hub_item: this.item_details.hub_item_code,
+				message
+			});
+		},
+		get_item_details(hub_item_code) {
+			return hub.call('get_item_details', { hub_item_code })
+		},
+		get_messages() {
+			if (!this.item_details) return [];
+			return hub.call('get_messages', {
+				against_seller: this.item_details.hub_seller,
+				against_item: this.item_details.hub_item_code
+			});
+		},
+		get_hub_item_code() {
+			return frappe.get_route()[2];
+		}
+	}
+}
+</script>
diff --git a/erpnext/public/js/hub/pages/Home.vue b/erpnext/public/js/hub/pages/Home.vue
index e91fb38..1d1973c 100644
--- a/erpnext/public/js/hub/pages/Home.vue
+++ b/erpnext/public/js/hub/pages/Home.vue
@@ -7,36 +7,34 @@
 			:placeholder="search_placeholder"
 			:on_search="set_search_route"
 			v-model="search_value"
-		>
-		</search-input>
+		/>
 
-		<div v-for="section in sections"
-			:key="section.title"
-		>
-			<div class="hub-items-header margin-bottom level">
+		<div v-for="section in sections" :key="section.title">
+
+			<section-header>
 				<h4>{{ section.title }}</h4>
 				<p :data-route="'marketplace/category/' + section.title">{{ 'See All' }}</p>
-			</div>
+			</section-header>
 
 			<item-cards-container
 				:container_name="section.title"
 				:items="section.items"
 				:item_id_fieldname="item_id_fieldname"
 				:on_click="go_to_item_details_page"
-			>
-			</item-cards-container>
+			/>
 		</div>
-
 	</div>
 </template>
 
 <script>
 import SearchInput from '../components/SearchInput.vue';
+import SectionHeader from '../components/SectionHeader.vue';
 import ItemCardsContainer from '../components/ItemCardsContainer.vue';
 
 export default {
 	name: 'home-page',
 	components: {
+		SectionHeader,
 		SearchInput,
 		ItemCardsContainer
 	},
diff --git a/erpnext/public/js/hub/vue-plugins.js b/erpnext/public/js/hub/vue-plugins.js
new file mode 100644
index 0000000..5555945
--- /dev/null
+++ b/erpnext/public/js/hub/vue-plugins.js
@@ -0,0 +1,44 @@
+import Vue from 'vue/dist/vue.js';
+Vue.prototype.__ = window.__;
+Vue.prototype.frappe = window.frappe;
+
+Vue.directive('route', {
+	bind(el, binding) {
+		const route = binding.value;
+		el.classList.add('cursor-pointer');
+		el.addEventListener('click', () => frappe.set_route(route));
+	},
+	unbind(el) {
+		el.classList.remove('cursor-pointer');
+	}
+});
+
+const handleImage = (el, src) => {
+	let img = new Image();
+	// add loading class
+	el.src = '';
+	el.classList.add('img-loading');
+
+	img.onload = () => {
+		// image loaded, remove loading class
+		el.classList.remove('img-loading');
+		// set src
+		el.src = src;
+	}
+	img.onerror = () => {
+		el.classList.remove('img-loading');
+		el.classList.add('no-image');
+		el.src = null;
+	}
+	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);
+	}
+});
diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less
index 779d45d..4cbe351 100644
--- a/erpnext/public/less/hub.less
+++ b/erpnext/public/less/hub.less
@@ -331,11 +331,10 @@
 		border-radius: 3px;
 		height: calc(100vh - 300px);
 		justify-content: space-between;
+		padding: 15px;
 	}
 
 	.message-list {
-		padding-top: 15px;
-		padding-bottom: 15px;
 		overflow: scroll;
 	}
 }