[hub] Register components, init Item Page

- commonly used components are pre-registered
- add DetailHeaderItem component
- begin ItemPage
diff --git a/erpnext/public/js/hub/PageContainer.vue b/erpnext/public/js/hub/PageContainer.vue
index ca4c63a..794d4cb 100644
--- a/erpnext/public/js/hub/PageContainer.vue
+++ b/erpnext/public/js/hub/PageContainer.vue
@@ -3,25 +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..5f346ff
--- /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: '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..eca31d1 100644
--- a/erpnext/public/js/hub/components/DetailView.vue
+++ b/erpnext/public/js/hub/components/DetailView.vue
@@ -31,16 +31,31 @@
 						<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="subtitle"></slot>
 					</div>
 
+					<button v-if="primary_action" class="btn btn-primary" @click="primary_action.action">
+						{{ primary_action.label }}
+					</button>
+				</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 +76,7 @@
 
 export default {
 	name: 'detail-view',
-	props: ['title', 'subtitles', 'image', 'sections', 'show_skeleton'],
+	props: ['title', 'subtitles', 'image', 'sections', 'show_skeleton', 'menu_items', 'primary_action'],
 	data() {
 		return {
 			back_to_home_text: __('Back to Home')
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 10a55aa..a442d4c 100644
--- a/erpnext/public/js/hub/marketplace.js
+++ b/erpnext/public/js/hub/marketplace.js
@@ -2,20 +2,10 @@
 import './vue-plugins';
 
 // pages
-import './pages/item';
 import './pages/messages';
 import './pages/buying_messages';
 
 import PageContainer from './PageContainer.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';
@@ -199,7 +189,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']) {
@@ -282,167 +272,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/Category.vue b/erpnext/public/js/hub/pages/Category.vue
index 2a521f4..5a23870 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 1d1973c..7019a62 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..fffd370
--- /dev/null
+++ b/erpnext/public/js/hub/pages/Item.vue
@@ -0,0 +1,150 @@
+<template>
+	<div
+		class="marketplace-page"
+		:data-page-name="page_name"
+		v-if="init || item"
+	>
+
+		<detail-view
+			:title="title"
+			:subtitles="subtitles"
+			:image="image"
+			:sections="sections"
+			:menu_items="menu_items"
+			:show_skeleton="init"
+		>
+			<detail-header-item slot="subtitle"
+				:value="item_subtitle"
+			></detail-header-item>
+			<detail-header-item slot="subtitle"
+				:value="item_views_and_ratings"
+			></detail-header-item>
+		</detail-view>
+	</div>
+</template>
+
+<script>
+import { get_rating_html } from '../components/reviews';
+
+export default {
+	name: 'item-page',
+	data() {
+		return {
+			page_name: frappe.get_route()[1],
+			hub_item_code: frappe.get_route()[2],
+
+			init: true,
+
+			item: null,
+			title: null,
+			subtitles: [],
+			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;
+		}
+	},
+	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(){
+			//
+		}
+	}
+}
+</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..9bbfe83 100644
--- a/erpnext/public/js/hub/pages/Profile.vue
+++ b/erpnext/public/js/hub/pages/Profile.vue
@@ -17,13 +17,8 @@
 </template>
 
 <script>
-import DetailView from '../components/DetailView.vue';
-
 export default {
 	name: 'profile-page',
-	components: {
-		DetailView
-	},
 	data() {
 		return {
 			page_name: frappe.get_route()[1],
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 db9057d..80fe8c0 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 f113a20..dd8b026 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 a7fbffc..fc4aff8 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 5a35812..eb9cab6 100644
--- a/erpnext/public/js/hub/pages/Seller.vue
+++ b/erpnext/public/js/hub/pages/Seller.vue
@@ -25,15 +25,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],
diff --git a/erpnext/public/js/hub/pages/messages.js b/erpnext/public/js/hub/pages/messages.js
index 6222f53..5f67502 100644
--- a/erpnext/public/js/hub/pages/messages.js
+++ b/erpnext/public/js/hub/pages/messages.js
@@ -2,7 +2,7 @@
 // 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';
+// import { get_empty_state } from '../components/empty_state';
 
 erpnext.hub.Buying = class Buying extends SubPage {
 	refresh() {
@@ -27,8 +27,8 @@
 	}
 
 	render_empty_state() {
-		const empty_state = get_empty_state(__('You haven\'t interacted with any seller yet.'));
-		this.$wrapper.html(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() {
diff --git a/erpnext/public/js/hub/vue-plugins.js b/erpnext/public/js/hub/vue-plugins.js
index 5555945..97bc66d 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;