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 ae9d460..bb9ba3e 100644
--- a/erpnext/public/js/hub/PageContainer.vue
+++ b/erpnext/public/js/hub/PageContainer.vue
@@ -3,26 +3,37 @@
 		<component :is="current_page"></component>
 	</div>
 </template>
+
 <script>
+
 import Home from './pages/Home.vue';
-import SavedProducts from './pages/SavedProducts.vue';
-import Publish from './pages/Publish.vue';
-import Category from './pages/Category.vue';
 import Search from './pages/Search.vue';
+import Category from './pages/Category.vue';
+import SavedProducts from './pages/SavedProducts.vue';
 import PublishedProducts from './pages/PublishedProducts.vue';
+import Item from './pages/Item.vue';
+import Seller from './pages/Seller.vue';
+import Publish from './pages/Publish.vue';
 import Buying from './pages/Buying.vue';
 import BuyingMessages from './pages/BuyingMessages.vue';
+import Profile from './pages/Profile.vue';
 import NotFound from './pages/NotFound.vue';
 
 const route_map = {
 	'marketplace/home': Home,
-	'marketplace/saved-products': SavedProducts,
-	'marketplace/my-products': PublishedProducts,
-	'marketplace/publish': Publish,
-	'marketplace/category/:category': Category,
 	'marketplace/search/:keyword': Search,
+	'marketplace/category/:category': Category,
+	'marketplace/item/:item': Item,
+	'marketplace/seller/:seller': Seller,
+	'marketplace/not-found': NotFound,
+
+	// Registered seller routes
+	'marketplace/profile': Profile,
+	'marketplace/saved-products': SavedProducts,
+	'marketplace/publish': Publish,
+	'marketplace/my-products': PublishedProducts,
 	'marketplace/buying': Buying,
-	'marketplace/buying/:item': BuyingMessages
+	'marketplace/buying/:item': BuyingMessages,
 }
 
 export default {
diff --git a/erpnext/public/js/hub/components/DetailHeaderItem.vue b/erpnext/public/js/hub/components/DetailHeaderItem.vue
new file mode 100644
index 0000000..8ca4379
--- /dev/null
+++ b/erpnext/public/js/hub/components/DetailHeaderItem.vue
@@ -0,0 +1,20 @@
+<template>
+	<p class="text-muted" v-html="header_item"></p>
+</template>
+
+<script>
+
+const spacer = '<span aria-hidden="true"> · </span>';
+
+export default {
+	name: 'detail-header-item',
+	props: ['value'],
+	data() {
+		return {
+			header_item: Array.isArray(this.value)
+				? this.value.join(spacer)
+				: this.value
+		}
+	},
+}
+</script>
diff --git a/erpnext/public/js/hub/components/DetailView.vue b/erpnext/public/js/hub/components/DetailView.vue
index b86468b..0e9e3f2 100644
--- a/erpnext/public/js/hub/components/DetailView.vue
+++ b/erpnext/public/js/hub/components/DetailView.vue
@@ -31,16 +31,27 @@
 						<img :src="image">
 					</div>
 				</div>
-				<div class="col-md-6">
+				<div class="col-md-8">
 					<h2>{{ title }}</h2>
 					<div class="text-muted">
-						<p v-for="subtitle in subtitles"
-							:key="subtitle"
-							v-html="subtitle"
-						>
-						</p>
+						<slot name="detail-header-item"></slot>
 					</div>
+				</div>
 
+				<div v-if="menu_items" class="col-md-1">
+					<div class="dropdown pull-right hub-item-dropdown">
+						<a class="dropdown-toggle btn btn-xs btn-default" data-toggle="dropdown">
+							<span class="caret"></span>
+						</a>
+						<ul class="dropdown-menu dropdown-right" role="menu">
+							<li v-for="menu_item in menu_items"
+								v-if="menu_item.condition"
+								:key="menu_item.label"
+							>
+								<a @click="menu_item.action">{{ menu_item.label }}</a>
+							</li>
+						</ul>
+					</div>
 				</div>
 			</div>
 			<div v-for="section in sections" class="row hub-item-description margin-bottom"
@@ -61,7 +72,7 @@
 
 export default {
 	name: 'detail-view',
-	props: ['title', 'subtitles', 'image', 'sections', 'show_skeleton'],
+	props: ['title', 'image', 'sections', 'show_skeleton', 'menu_items'],
 	data() {
 		return {
 			back_to_home_text: __('Back to Home')
diff --git a/erpnext/public/js/hub/components/ReviewArea.vue b/erpnext/public/js/hub/components/ReviewArea.vue
new file mode 100644
index 0000000..fd2f145
--- /dev/null
+++ b/erpnext/public/js/hub/components/ReviewArea.vue
@@ -0,0 +1,42 @@
+<template>
+	<div>
+		<div ref="review-area"></div>
+	</div>
+</template>
+<script>
+export default {
+	props: ['hub_item_code'],
+	mounted() {
+		this.make_input();
+	},
+	methods: {
+		make_input() {
+			this.comment_area = new frappe.ui.ReviewArea({
+				parent: this.$refs['review-area'],
+				mentions: [],
+				on_submit: this.on_submit_review.bind(this)
+			});
+
+			this.message_input = new frappe.ui.CommentArea({
+				parent: this.$refs['review-area'],
+				on_submit: (message) => {
+					this.message_input.reset();
+					this.$emit('change', message);
+				},
+				no_wrapper: true
+			});
+		},
+
+		on_submit_review(values) {
+			values.user = frappe.session.user;
+			values.username = frappe.session.user_fullname;
+
+			hub.call('add_item_review', {
+				hub_item_code: this.hub_item_code,
+				review: JSON.stringify(values)
+			})
+			// .then(this.push_review_in_review_area.bind(this));
+		}
+	}
+}
+</script>
diff --git a/erpnext/public/js/hub/components/ReviewTimelineItem.vue b/erpnext/public/js/hub/components/ReviewTimelineItem.vue
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/public/js/hub/components/ReviewTimelineItem.vue
diff --git a/erpnext/public/js/hub/marketplace.js b/erpnext/public/js/hub/marketplace.js
index 663e803..95a7542 100644
--- a/erpnext/public/js/hub/marketplace.js
+++ b/erpnext/public/js/hub/marketplace.js
@@ -1,22 +1,9 @@
 import Vue from 'vue/dist/vue.js';
 import './vue-plugins';
 
-// pages
-import './pages/item';
-
+// components
 import PageContainer from './PageContainer.vue';
 import Sidebar from './Sidebar.vue';
-import Home from './pages/Home.vue';
-import SavedProducts from './pages/SavedProducts.vue';
-import Publish from './pages/Publish.vue';
-import Category from './pages/Category.vue';
-import Search from './pages/Search.vue';
-import PublishedProducts from './pages/PublishedProducts.vue';
-import Profile from './pages/Profile.vue';
-import Seller from './pages/Seller.vue';
-import NotFound from './pages/NotFound.vue';
-
-// components
 import { ProfileDialog } from './components/profile_dialog';
 
 // helpers
@@ -117,7 +104,7 @@
 		}
 
 		if (route[1] === 'item' && route[2] && !this.subpages.item) {
-			this.subpages.item = new erpnext.hub.Item(this.$body);
+			this.subpages.item = new erpnext.hub.ItemPage(this.$body);
 		}
 
 		if (route[1] === 'seller' && !this.subpages['seller']) {
@@ -200,167 +187,3 @@
 		});
 	}
 }
-
-erpnext.hub.HomePage = class {
-	constructor(parent) {
-		this.$wrapper = $(`<div id="vue-area-home">`).appendTo($(parent));
-
-		new Vue({
-			render: h => h(Home)
-		}).$mount('#vue-area-home');
-	}
-
-	show() {
-		$('[data-page-name="home"]').show();
-	}
-
-	hide() {
-		$('[data-page-name="home"]').hide();
-	}
-}
-
-erpnext.hub.SavedProductsPage = class {
-	constructor(parent) {
-		this.$wrapper = $(`<div id="vue-area-saved">`).appendTo($(parent));
-
-		new Vue({
-			render: h => h(SavedProducts)
-		}).$mount('#vue-area-saved');
-	}
-
-	show() {
-		$('[data-page-name="saved-products"]').show();
-	}
-
-	hide() {
-		$('[data-page-name="saved-products"]').hide();
-	}
-}
-
-erpnext.hub.PublishPage = class {
-	constructor(parent) {
-		this.$wrapper = $(`<div id="vue-area">`).appendTo($(parent));
-
-		new Vue({
-			render: h => h(Publish)
-		}).$mount('#vue-area');
-	}
-
-	show() {
-		$('[data-page-name="publish"]').show();
-	}
-
-	hide() {
-		$('[data-page-name="publish"]').hide();
-	}
-
-}
-
-erpnext.hub.CategoryPage = class {
-	constructor(parent) {
-		this.$wrapper = $(`<div id="vue-area-category">`).appendTo($(parent));
-
-		new Vue({
-			render: h => h(Category)
-		}).$mount('#vue-area-category');
-	}
-
-	show() {
-		$('[data-page-name="category"]').show();
-	}
-
-	hide() {
-		$('[data-page-name="category"]').hide();
-	}
-}
-
-erpnext.hub.PublishedProductsPage = class {
-	constructor(parent) {
-		this.$wrapper = $(`<div id="vue-area-published-products">`).appendTo($(parent));
-
-		new Vue({
-			render: h => h(PublishedProducts)
-		}).$mount('#vue-area-published-products');
-	}
-
-	show() {
-		$('[data-page-name="published-products"]').show();
-	}
-
-	hide() {
-		$('[data-page-name="published-products"]').hide();
-	}
-}
-
-erpnext.hub.SearchPage = class {
-	constructor(parent) {
-		this.$wrapper = $(`<div id="vue-area-search">`).appendTo($(parent));
-
-		new Vue({
-			render: h => h(Search)
-		}).$mount('#vue-area-search');
-	}
-
-	show() {
-		$('[data-page-name="search"]').show();
-	}
-
-	hide() {
-		$('[data-page-name="search"]').hide();
-	}
-}
-
-erpnext.hub.ProfilePage = class {
-	constructor(parent) {
-		this.$wrapper = $(`<div id="vue-area-profile">`).appendTo($(parent));
-
-		new Vue({
-			render: h => h(Profile)
-		}).$mount('#vue-area-profile');
-	}
-
-	show() {
-		$('[data-page-name="profile"]').show();
-	}
-
-	hide() {
-		$('[data-page-name="profile"]').hide();
-	}
-}
-
-erpnext.hub.SellerPage = class {
-	constructor(parent) {
-		this.$wrapper = $(`<div id="vue-area-seller">`).appendTo($(parent));
-
-		new Vue({
-			render: h => h(Seller)
-		}).$mount('#vue-area-seller');
-	}
-
-	show() {
-		$('[data-page-name="seller"]').show();
-	}
-
-	hide() {
-		$('[data-page-name="seller"]').hide();
-	}
-}
-
-erpnext.hub.NotFoundPage = class {
-	constructor(parent) {
-		this.$wrapper = $(`<div id="vue-area-not-found">`).appendTo($(parent));
-
-		new Vue({
-			render: h => h(NotFound)
-		}).$mount('#vue-area-not-found');
-	}
-
-	show() {
-		$('[data-page-name="not-found"]').show();
-	}
-
-	hide() {
-		$('[data-page-name="not-found"]').hide();
-	}
-}
-
diff --git a/erpnext/public/js/hub/pages/BuyingMessages.vue b/erpnext/public/js/hub/pages/BuyingMessages.vue
index 6f1a4c5..49a6656 100644
--- a/erpnext/public/js/hub/pages/BuyingMessages.vue
+++ b/erpnext/public/js/hub/pages/BuyingMessages.vue
@@ -30,13 +30,11 @@
 	</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
 	},
diff --git a/erpnext/public/js/hub/pages/Category.vue b/erpnext/public/js/hub/pages/Category.vue
index c11972d..2ffbcf3 100644
--- a/erpnext/public/js/hub/pages/Category.vue
+++ b/erpnext/public/js/hub/pages/Category.vue
@@ -17,13 +17,8 @@
 </template>
 
 <script>
-import ItemCardsContainer from '../components/ItemCardsContainer.vue';
-
 export default {
 	name: 'saved-products-page',
-	components: {
-		ItemCardsContainer
-	},
 	data() {
 		return {
 			page_name: frappe.get_route()[1],
diff --git a/erpnext/public/js/hub/pages/Home.vue b/erpnext/public/js/hub/pages/Home.vue
index d09058d..bba2e1d 100644
--- a/erpnext/public/js/hub/pages/Home.vue
+++ b/erpnext/public/js/hub/pages/Home.vue
@@ -27,17 +27,8 @@
 </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
-	},
 	data() {
 		return {
 			page_name: frappe.get_route()[1],
diff --git a/erpnext/public/js/hub/pages/Item.vue b/erpnext/public/js/hub/pages/Item.vue
new file mode 100644
index 0000000..ad28f42
--- /dev/null
+++ b/erpnext/public/js/hub/pages/Item.vue
@@ -0,0 +1,205 @@
+<template>
+	<div
+		class="marketplace-page"
+		:data-page-name="page_name"
+		v-if="init || item"
+	>
+
+		<detail-view
+			:title="title"
+			:image="image"
+			:sections="sections"
+			:menu_items="menu_items"
+			:show_skeleton="init"
+		>
+			<detail-header-item slot="detail-header-item"
+				:value="item_subtitle"
+			></detail-header-item>
+			<detail-header-item slot="detail-header-item"
+				:value="item_views_and_ratings"
+			></detail-header-item>
+
+			<button slot="detail-header-item"
+				class="btn btn-primary margin-top"
+				@click="primary_action.action"
+			>
+				{{ primary_action.label }}
+			</button>
+
+		</detail-view>
+
+		<!-- <review-area :hub_item_code="hub_item_code"></review-area> -->
+	</div>
+</template>
+
+<script>
+import ReviewArea from '../components/ReviewArea.vue';
+import { get_rating_html } from '../components/reviews';
+
+export default {
+	name: 'item-page',
+	components: {
+		ReviewArea
+	},
+	data() {
+		return {
+			page_name: frappe.get_route()[1],
+			hub_item_code: frappe.get_route()[2],
+
+			init: true,
+
+			item: null,
+			title: null,
+			image: null,
+			sections: [],
+
+			menu_items: [
+				{
+					label: __('Report this Product'),
+					condition: !this.is_own_item,
+					action: this.report_item
+				},
+				{
+					label: __('Edit Details'),
+					condition: this.is_own_item,
+					action: this.report_item
+				},
+				{
+					label: __('Unpublish Product'),
+					condition: this.is_own_item,
+					action: this.report_item
+				}
+			]
+		};
+	},
+	computed: {
+		is_own_item() {
+			let is_own_item = false;
+			if(this.item) {
+				if(this.item.hub_seller === hub.setting.company_email) {
+					is_own_item = true;
+				}
+			}
+			return is_own_item;
+		},
+
+		item_subtitle() {
+			if(!this.item) {
+				return '';
+			}
+
+			const dot_spacer = '<span aria-hidden="true"> · </span>';
+			let subtitle_items = [comment_when(this.item.creation)];
+			const rating = this.item.average_rating;
+
+			if (rating > 0) {
+				subtitle_items.push(rating + `<i class='fa fa-fw fa-star-o'></i>`)
+			}
+
+			subtitle_items.push(this.item.company);
+
+			return subtitle_items;
+		},
+
+		item_views_and_ratings() {
+			if(!this.item) {
+				return '';
+			}
+
+			let stats = __('No views yet');
+			if(this.item.view_count) {
+				const views_message = __(`${this.item.view_count} Views`);
+
+				const rating_html = get_rating_html(this.item.average_rating);
+				const rating_count = this.item.no_of_ratings > 0 ? `${this.item.no_of_ratings} reviews` : __('No reviews yet');
+
+				stats = [views_message, rating_html, rating_count];
+			}
+
+			return stats;
+		},
+
+		primary_action() {
+			return {
+				label: __('Contact Seller'),
+				action: this.contact_seller.bind(this)
+			}
+		}
+	},
+	created() {
+		this.get_profile();
+	},
+	methods: {
+		get_profile() {
+			hub.call('get_item_details',{ hub_item_code: this.hub_item_code })
+				.then(item => {
+				this.init = false;
+				this.item = item;
+
+				this.build_data();
+			});
+		},
+
+		build_data() {
+			this.title = this.item.item_name || this.item.name;
+
+			this.image = this.item.image;
+
+			this.sections = [
+				{
+					title: __('Product Description'),
+					content: this.item.description
+						? __(this.item.description)
+						: __('No description')
+				},
+				{
+					title: __('Seller Information'),
+					content: ''
+				}
+			];
+		},
+
+		report_item() {
+			//
+		},
+
+		contact_seller() {
+			const d = new frappe.ui.Dialog({
+				title: __('Send a message'),
+				fields: [
+					{
+						fieldname: 'to',
+						fieldtype: 'Read Only',
+						label: __('To'),
+						default: this.item.company
+					},
+					{
+						fieldtype: 'Text',
+						fieldname: 'message',
+						label: __('Message')
+					}
+				],
+				primary_action: ({ message }) => {
+					if (!message) return;
+
+					hub.call('send_message', {
+						from_seller: hub.settings.company_email,
+						to_seller: this.item.hub_seller,
+						hub_item: this.item.hub_item_code,
+						message
+					})
+						.then(() => {
+							d.hide();
+							frappe.set_route('marketplace', 'buy', this.item.hub_item_code);
+							erpnext.hub.trigger('action:send_message')
+						});
+				}
+			});
+
+			d.show();
+		}
+	}
+}
+</script>
+
+<style scoped></style>
diff --git a/erpnext/public/js/hub/pages/NotFound.vue b/erpnext/public/js/hub/pages/NotFound.vue
index 7a76437..246d31b 100644
--- a/erpnext/public/js/hub/pages/NotFound.vue
+++ b/erpnext/public/js/hub/pages/NotFound.vue
@@ -14,13 +14,8 @@
 </template>
 
 <script>
-import EmptyState from '../components/EmptyState.vue';
-
 export default {
 	name: 'not-found-page',
-	components: {
-		EmptyState
-	},
 	data() {
 		return {
 			page_name: 'not-found',
diff --git a/erpnext/public/js/hub/pages/Profile.vue b/erpnext/public/js/hub/pages/Profile.vue
index 1767796..a0bc6cb 100644
--- a/erpnext/public/js/hub/pages/Profile.vue
+++ b/erpnext/public/js/hub/pages/Profile.vue
@@ -7,23 +7,28 @@
 
 		<detail-view
 			:title="title"
-			:subtitles="subtitles"
 			:image="image"
 			:sections="sections"
 			:show_skeleton="init"
 		>
+
+			<detail-header-item slot="detail-header-item"
+				:value="country"
+			></detail-header-item>
+			<detail-header-item slot="detail-header-item"
+				:value="site_name"
+			></detail-header-item>
+			<detail-header-item slot="detail-header-item"
+				:value="joined_when"
+			></detail-header-item>
+
 		</detail-view>
 	</div>
 </template>
 
 <script>
-import DetailView from '../components/DetailView.vue';
-
 export default {
 	name: 'profile-page',
-	components: {
-		DetailView
-	},
 	data() {
 		return {
 			page_name: frappe.get_route()[1],
@@ -32,9 +37,12 @@
 
 			profile: null,
 			title: null,
-			subtitles: [],
 			image: null,
-			sections: []
+			sections: [],
+
+			country: '',
+			site_name: '',
+			joined_when: '',
 		};
 	},
 	created() {
@@ -50,11 +58,11 @@
 
 				this.profile = profile;
 				this.title = profile.company;
-				this.subtitles = [
-					__(profile.country),
-					__(profile.site_name),
-					__(`Joined ${comment_when(profile.creation)}`)
-				];
+
+				this.country = __(profile.country);
+				this.site_name = __(profile.site_name);
+				this.joined_when = __(`Joined ${comment_when(profile.creation)}`);
+
 				this.image = profile.logo;
 				this.sections = [
 					{
diff --git a/erpnext/public/js/hub/pages/Publish.vue b/erpnext/public/js/hub/pages/Publish.vue
index b801b92..788d7ba 100644
--- a/erpnext/public/js/hub/pages/Publish.vue
+++ b/erpnext/public/js/hub/pages/Publish.vue
@@ -54,13 +54,14 @@
 </template>
 
 <script>
-import SearchInput from '../components/SearchInput.vue';
-import ItemCardsContainer from '../components/ItemCardsContainer.vue';
 import NotificationMessage from '../components/NotificationMessage.vue';
 import { ItemPublishDialog } from '../components/item_publish_dialog';
 
 export default {
 	name: 'publish-page',
+	components: {
+		NotificationMessage
+	},
 	data() {
 		return {
 			page_name: frappe.get_route()[1],
@@ -87,11 +88,6 @@
 				: ''
 		};
 	},
-	components: {
-		SearchInput,
-		ItemCardsContainer,
-		NotificationMessage
-	},
 	computed: {
 		no_selected_items() {
 			return this.selected_items.length === 0;
diff --git a/erpnext/public/js/hub/pages/PublishedProducts.vue b/erpnext/public/js/hub/pages/PublishedProducts.vue
index 586d701..561dab2 100644
--- a/erpnext/public/js/hub/pages/PublishedProducts.vue
+++ b/erpnext/public/js/hub/pages/PublishedProducts.vue
@@ -17,13 +17,8 @@
 </template>
 
 <script>
-import ItemCardsContainer from '../components/ItemCardsContainer.vue';
-
 export default {
 	name: 'saved-products-page',
-	components: {
-		ItemCardsContainer
-	},
 	data() {
 		return {
 			page_name: frappe.get_route()[1],
diff --git a/erpnext/public/js/hub/pages/SavedProducts.vue b/erpnext/public/js/hub/pages/SavedProducts.vue
index e83bfd5..d55e9dc 100644
--- a/erpnext/public/js/hub/pages/SavedProducts.vue
+++ b/erpnext/public/js/hub/pages/SavedProducts.vue
@@ -19,8 +19,6 @@
 </template>
 
 <script>
-import ItemCardsContainer from '../components/ItemCardsContainer.vue';
-
 export default {
 	name: 'saved-products-page',
 	data() {
@@ -34,9 +32,6 @@
 			empty_state_message: __(`You haven't favourited any items yet.`)
 		};
 	},
-	components: {
-		ItemCardsContainer
-	},
 	created() {
 		this.get_items();
 	},
diff --git a/erpnext/public/js/hub/pages/Search.vue b/erpnext/public/js/hub/pages/Search.vue
index b9ddc0d..809f8c2 100644
--- a/erpnext/public/js/hub/pages/Search.vue
+++ b/erpnext/public/js/hub/pages/Search.vue
@@ -24,15 +24,8 @@
 </template>
 
 <script>
-import SearchInput from '../components/SearchInput.vue';
-import ItemCardsContainer from '../components/ItemCardsContainer.vue';
-
 export default {
 	name: 'saved-products-page',
-	components: {
-		SearchInput,
-		ItemCardsContainer
-	},
 	data() {
 		return {
 			page_name: frappe.get_route()[1],
diff --git a/erpnext/public/js/hub/pages/Seller.vue b/erpnext/public/js/hub/pages/Seller.vue
index 04c9de1..86d75b8 100644
--- a/erpnext/public/js/hub/pages/Seller.vue
+++ b/erpnext/public/js/hub/pages/Seller.vue
@@ -6,11 +6,20 @@
 	>
 		<detail-view
 			:title="title"
-			:subtitles="subtitles"
 			:image="image"
 			:sections="sections"
 			:show_skeleton="init"
 		>
+			<detail-header-item slot="detail-header-item"
+				:value="country"
+			></detail-header-item>
+			<detail-header-item slot="detail-header-item"
+				:value="site_name"
+			></detail-header-item>
+			<detail-header-item slot="detail-header-item"
+				:value="joined_when"
+			></detail-header-item>
+
 		</detail-view>
 
 		<h5 v-if="profile">{{ item_container_heading }}</h5>
@@ -25,15 +34,8 @@
 </template>
 
 <script>
-import DetailView from '../components/DetailView.vue';
-import ItemCardsContainer from '../components/ItemCardsContainer.vue';
-
 export default {
 	name: 'seller-page',
-	components: {
-		DetailView,
-		ItemCardsContainer
-	},
 	data() {
 		return {
 			page_name: frappe.get_route()[1],
@@ -46,9 +48,12 @@
 			item_id_fieldname: 'name',
 
 			title: null,
-			subtitles: [],
 			image: null,
 			sections: [],
+
+			country: '',
+			site_name: '',
+			joined_when: '',
 		};
 	},
 	created() {
@@ -72,11 +77,11 @@
 				const profile = this.profile;
 
 				this.title = profile.company;
-				this.subtitles = [
-					__(profile.country),
-					__(profile.site_name),
-					__(`Joined ${comment_when(profile.creation)}`)
-				];
+
+				this.country = __(profile.country);
+				this.site_name = __(profile.site_name);
+				this.joined_when = __(`Joined ${comment_when(profile.creation)}`);
+
 				this.image = profile.logo;
 				this.sections = [
 					{
diff --git a/erpnext/public/js/hub/pages/messages.js b/erpnext/public/js/hub/pages/messages.js
new file mode 100644
index 0000000..5f67502
--- /dev/null
+++ b/erpnext/public/js/hub/pages/messages.js
@@ -0,0 +1,104 @@
+import SubPage from './subpage';
+// import { get_item_card_container_html } from '../components/items_container';
+import { get_buying_item_message_card_html } from '../components/item_card';
+import { get_selling_item_message_card_html } from '../components/item_card';
+// import { get_empty_state } from '../components/empty_state';
+
+erpnext.hub.Buying = class Buying extends SubPage {
+	refresh() {
+		this.get_items_for_messages().then((items) => {
+			this.empty();
+			if (items.length) {
+				items.map(item => {
+					item.route = `marketplace/buying/${item.hub_item_code}`
+				})
+				this.render(items, __('Buying'));
+			}
+
+			if (!items.length && !items.length) {
+				this.render_empty_state();
+			}
+		});
+	}
+
+	render(items = [], title) {
+		// const html = get_item_card_container_html(items, title, get_buying_item_message_card_html);
+		this.$wrapper.append(html);
+	}
+
+	render_empty_state() {
+		// const empty_state = get_empty_state(__('You haven\'t interacted with any seller yet.'));
+		// this.$wrapper.html(empty_state);
+	}
+
+	get_items_for_messages() {
+		return hub.call('get_buying_items_for_messages', {}, 'action:send_message');
+	}
+}
+
+erpnext.hub.Selling = class Selling extends SubPage {
+	refresh() {
+		this.get_items_for_messages().then((items) => {
+			this.empty();
+			if (items.length) {
+				items.map(item => {
+					item.route = `marketplace/selling/${item.hub_item_code}`
+				})
+				this.render(items, __('Selling'));
+			}
+
+			if (!items.length && !items.length) {
+				this.render_empty_state();
+			}
+		});
+	}
+
+	render(items = [], title) {
+		// const html = get_item_card_container_html(items, title, get_selling_item_message_card_html);
+		this.$wrapper.append(html);
+	}
+
+	render_empty_state() {
+		const empty_state = get_empty_state(__('You haven\'t interacted with any seller yet.'));
+		this.$wrapper.html(empty_state);
+	}
+
+	get_items_for_messages() {
+		return hub.call('get_selling_items_for_messages', {});
+	}
+}
+
+function get_message_area_html() {
+	return `
+		<div class="message-area border padding flex flex-column">
+			<div class="message-list">
+			</div>
+			<div class="message-input">
+			</div>
+		</div>
+	`;
+}
+
+function get_list_item_html(seller) {
+	const active_class = frappe.get_route()[2] === seller.email ? 'active' : '';
+
+	return `
+		<div class="message-list-item ${active_class}" data-route="marketplace/messages/${seller.email}">
+			<div class="list-item-left">
+				<img src="${seller.image || 'https://picsum.photos/200?random'}">
+			</div>
+			<div class="list-item-body">
+				${seller.company}
+			</div>
+		</div>
+	`;
+}
+
+function get_message_html(message) {
+	return `
+		<div>
+			<h5>${message.sender}</h5>
+			<p>${message.content}</p>
+		</div>
+	`;
+}
diff --git a/erpnext/public/js/hub/vue-plugins.js b/erpnext/public/js/hub/vue-plugins.js
index 00b29b0..7d0619f 100644
--- a/erpnext/public/js/hub/vue-plugins.js
+++ b/erpnext/public/js/hub/vue-plugins.js
@@ -1,7 +1,23 @@
 import Vue from 'vue/dist/vue.js';
+
+// Global components
+import ItemCardsContainer from './components/ItemCardsContainer.vue';
+import SectionHeader from './components/SectionHeader.vue';
+import SearchInput from './components/SearchInput.vue';
+import DetailView from './components/DetailView.vue';
+import DetailHeaderItem from './components/DetailHeaderItem.vue';
+import EmptyState from './components/EmptyState.vue';
+
 Vue.prototype.__ = window.__;
 Vue.prototype.frappe = window.frappe;
 
+Vue.component('item-cards-container', ItemCardsContainer);
+Vue.component('section-header', SectionHeader);
+Vue.component('search-input', SearchInput);
+Vue.component('detail-view', DetailView);
+Vue.component('detail-header-item', DetailHeaderItem);
+Vue.component('empty-state', EmptyState);
+
 Vue.directive('route', {
 	bind(el, binding) {
 		const route = binding.value;