Break marketplace.js into multiple files
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index ed4ebab..7bcf99b 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -7,6 +7,9 @@
         "public/js/website_utils.js",
         "public/js/shopping_cart.js"
     ],
+    "js/marketplace.min.js": [
+        "public/js/hub/marketplace.js"
+    ],
     "js/erpnext.min.js": [
         "public/js/conf.js",
         "public/js/utils.js",
diff --git a/erpnext/public/js/hub/helpers.js b/erpnext/public/js/hub/helpers.js
new file mode 100644
index 0000000..22e35c3
--- /dev/null
+++ b/erpnext/public/js/hub/helpers.js
@@ -0,0 +1,143 @@
+function get_empty_state(message, action) {
+	return `<div class="empty-state flex align-center flex-column justify-center">
+		<p class="text-muted">${message}</p>
+		${action ? `<p>${action}</p>`: ''}
+	</div>`;
+}
+
+function get_item_card_container_html(items, title='', get_item_html = get_item_card_html) {
+	const items_html = (items || []).map(item => get_item_html(item)).join('');
+	const title_html = title
+		? `<div class="col-sm-12 margin-bottom">
+				<b>${title}</b>
+			</div>`
+		: '';
+
+	const html = `<div class="row hub-card-container">
+		${title_html}
+		${items_html}
+	</div>`;
+
+	return html;
+}
+
+function get_item_card_html(item) {
+	const item_name = item.item_name || item.name;
+	const title = strip_html(item_name);
+	const img_url = item.image;
+	const company_name = item.company;
+
+	// Subtitle
+	let subtitle = [comment_when(item.creation)];
+	const rating = item.average_rating;
+	if (rating > 0) {
+		subtitle.push(rating + `<i class='fa fa-fw fa-star-o'></i>`)
+	}
+	subtitle.push(company_name);
+
+	let dot_spacer = '<span aria-hidden="true"> · </span>';
+	subtitle = subtitle.join(dot_spacer);
+
+	const item_html = `
+		<div class="col-md-3 col-sm-4 col-xs-6">
+			<div class="hub-card" data-route="marketplace/item/${item.hub_item_code}">
+				<div class="hub-card-header">
+					<div class="hub-card-title ellipsis bold">${title}</div>
+					<div class="hub-card-subtitle ellipsis text-muted">${subtitle}</div>
+				</div>
+				<div class="hub-card-body">
+					<img class="hub-card-image" src="${img_url}" />
+					<div class="overlay hub-card-overlay"></div>
+				</div>
+			</div>
+		</div>
+	`;
+
+	return item_html;
+}
+
+function get_local_item_card_html(item) {
+	const item_name = item.item_name || item.name;
+	const title = strip_html(item_name);
+	const img_url = item.image;
+	const company_name = item.company;
+
+	const is_active = item.publish_in_hub;
+	const id = item.hub_item_code || item.item_code;
+
+	// Subtitle
+	let subtitle = [comment_when(item.creation)];
+	const rating = item.average_rating;
+	if (rating > 0) {
+		subtitle.push(rating + `<i class='fa fa-fw fa-star-o'></i>`)
+	}
+	subtitle.push(company_name);
+
+	let dot_spacer = '<span aria-hidden="true"> · </span>';
+	subtitle = subtitle.join(dot_spacer);
+
+	const edit_item_button = `<div class="hub-card-overlay-button" style="right: 15px; bottom: 15px;" data-route="Form/Item/${item.item_name}">
+		<button class="btn btn-default zoom-view">
+			<i class="octicon octicon-pencil text-muted"></i>
+		</button>
+	</div>`;
+
+	const item_html = `
+		<div class="col-md-3 col-sm-4 col-xs-6">
+			<div class="hub-card is-local ${is_active ? 'active' : ''}" data-id="${id}">
+				<div class="hub-card-header">
+					<div class="hub-card-title ellipsis bold">${title}</div>
+					<div class="hub-card-subtitle ellipsis text-muted">${subtitle}</div>
+					<i class="octicon octicon-check text-success"></i>
+				</div>
+				<div class="hub-card-body">
+					<img class="hub-card-image" src="${img_url}" />
+					<div class="hub-card-overlay">
+						<div class="hub-card-overlay-body">
+							${edit_item_button}
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+	`;
+
+	return item_html;
+}
+
+
+function get_rating_html(rating) {
+	let rating_html = ``;
+	for (var i = 0; i < 5; i++) {
+		let star_class = 'fa-star';
+		if (i >= rating) star_class = 'fa-star-o';
+		rating_html += `<i class='fa fa-fw ${star_class} star-icon' data-index=${i}></i>`;
+	}
+	return rating_html;
+}
+
+function make_search_bar({wrapper, on_search, placeholder = __('Search for anything')}) {
+	const $search = $(`
+		<div class="hub-search-container">
+			<input type="text" class="form-control" placeholder="${placeholder}">
+		</div>`
+	);
+	wrapper.append($search);
+	const $search_input = $search.find('input');
+
+	$search_input.on('keydown', frappe.utils.debounce((e) => {
+		if (e.which === frappe.ui.keyCode.ENTER) {
+			const search_value = $search_input.val();
+			on_search(search_value);
+		}
+	}, 300));
+}
+
+export {
+	get_empty_state,
+	get_item_card_container_html,
+	get_item_card_html,
+	get_local_item_card_html,
+	get_rating_html,
+	make_search_bar,
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/hub_call.js b/erpnext/public/js/hub/hub_call.js
new file mode 100644
index 0000000..e0fead3
--- /dev/null
+++ b/erpnext/public/js/hub/hub_call.js
@@ -0,0 +1,44 @@
+frappe.provide('hub');
+frappe.provide('erpnext.hub');
+
+erpnext.hub.cache = {};
+hub.call = function call_hub_method(method, args={}) {
+	return new Promise((resolve, reject) => {
+
+		// cache
+		const key = method + JSON.stringify(args);
+		if (erpnext.hub.cache[key]) {
+			resolve(erpnext.hub.cache[key]);
+		}
+
+		// cache invalidation after 5 minutes
+		const timeout = 5 * 60 * 1000;
+
+		setTimeout(() => {
+			delete erpnext.hub.cache[key];
+		}, timeout);
+
+		frappe.call({
+			method: 'erpnext.hub_node.call_hub_method',
+			args: {
+				method,
+				params: args
+			}
+		})
+		.then(r => {
+			if (r.message) {
+				if (r.message.error) {
+					frappe.throw({
+						title: __('Marketplace Error'),
+						message: r.message.error
+					});
+				}
+
+				erpnext.hub.cache[key] = r.message;
+				resolve(r.message)
+			}
+			reject(r)
+		})
+		.fail(reject)
+	});
+}
diff --git a/erpnext/public/js/hub/hub_factory.js b/erpnext/public/js/hub/hub_factory.js
index c94edf4..b073720 100644
--- a/erpnext/public/js/hub/hub_factory.js
+++ b/erpnext/public/js/hub/hub_factory.js
@@ -1,9 +1,7 @@
-frappe.provide('erpnext.hub.pages');
+frappe.provide('erpnext.hub');
 
 frappe.views.marketplaceFactory = class marketplaceFactory extends frappe.views.Factory {
 	show() {
-		const page_name = frappe.get_route_str();
-
 		if (frappe.pages.marketplace) {
 			frappe.container.change_to('marketplace');
 			erpnext.hub.marketplace.refresh();
@@ -14,7 +12,7 @@
 
 	make(page_name) {
 		const assets = [
-			'/assets/erpnext/js/hub/marketplace.js'
+			'/assets/js/marketplace.min.js'
 		];
 
 		frappe.require(assets, () => {
@@ -24,83 +22,3 @@
 		});
 	}
 }
-
-frappe.views.HubFactory = class HubFactory extends frappe.views.Factory {
-
-	make(route) {
-		const page_name = frappe.get_route_str();
-		const page = route[1];
-
-		const assets = {
-			'List': [
-				'/assets/erpnext/js/hub/hub_listing.js',
-			],
-			'Form': [
-				'/assets/erpnext/js/hub/hub_form.js'
-			]
-		};
-		frappe.model.with_doc('Hub Settings', 'Hub Settings', () => {
-			this.hub_settings = frappe.get_doc('Hub Settings');
-
-			if (!erpnext.hub.pages[page_name]) {
-				if(!frappe.is_online()) {
-					this.render_offline_card();
-					return;
-				}
-				if (!route[2]) {
-					frappe.require(assets['List'], () => {
-						if(page === 'Favourites') {
-							erpnext.hub.pages[page_name] = new erpnext.hub['Favourites']({
-								parent: this.make_page(true, page_name),
-								hub_settings: this.hub_settings
-							});
-						} else {
-							erpnext.hub.pages[page_name] = new erpnext.hub[page+'Listing']({
-								parent: this.make_page(true, page_name),
-								hub_settings: this.hub_settings
-							});
-						}
-					});
-				} else if (!route[3]){
-					frappe.require(assets['Form'], () => {
-						erpnext.hub.pages[page_name] = new erpnext.hub[page+'Page']({
-							unique_id: route[2],
-							doctype: route[2],
-							parent: this.make_page(true, page_name),
-							hub_settings: this.hub_settings
-						});
-					});
-				} else {
-					frappe.require(assets['List'], () => {
-						frappe.route_options = {};
-						frappe.route_options["company_name"] = route[2]
-						erpnext.hub.pages[page_name] = new erpnext.hub['ItemListing']({
-							parent: this.make_page(true, page_name),
-							hub_settings: this.hub_settings
-						});
-					});
-				}
-				window.hub_page = erpnext.hub.pages[page_name];
-			} else {
-				frappe.container.change_to(page_name);
-				window.hub_page = erpnext.hub.pages[page_name];
-			}
-		});
-	}
-
-	render_offline_card() {
-		let html = `<div class='page-card' style='margin: 140px auto;'>
-			<div class='page-card-head'>
-				<span class='indicator red'>${'Failed to connect'}</span>
-			</div>
-			<p>${ __("Please check your network connection.") }</p>
-			<div><a href='#Hub/Item' class='btn btn-primary btn-sm'>
-				${ __("Reload") }</a></div>
-		</div>`;
-
-		let page = $('#body_div');
-		page.append(html);
-
-		return;
-	}
-}
diff --git a/erpnext/public/js/hub/hub_form.js b/erpnext/public/js/hub/hub_form.js
deleted file mode 100644
index 9287e6d..0000000
--- a/erpnext/public/js/hub/hub_form.js
+++ /dev/null
@@ -1,493 +0,0 @@
-frappe.provide('erpnext.hub');
-
-erpnext.hub.HubDetailsPage = class HubDetailsPage extends frappe.views.BaseList {
-	setup_defaults() {
-		super.setup_defaults();
-		this.method = 'erpnext.hub_node.get_details';
-		const route = frappe.get_route();
-		// this.page_name = route[2];
-	}
-
-	setup_fields() {
-		return this.get_meta()
-			.then(r => {
-				this.meta = r.message.meta || this.meta;
-				this.categories = r.message.categories || [];
-				this.bootstrap_data(r.message);
-
-				this.getFormFields();
-			});
-	}
-
-	bootstrap_data() { }
-
-	get_meta() {
-		return new Promise(resolve =>
-			frappe.call('erpnext.hub_node.get_meta', {doctype: 'Hub ' + this.doctype}, resolve));
-	}
-
-
-	set_breadcrumbs() {
-		frappe.breadcrumbs.add({
-			label: __('Hub'),
-			route: '#Hub/' + this.doctype,
-			type: 'Custom'
-		});
-	}
-
-	setup_side_bar() {
-		this.sidebar = new frappe.ui.Sidebar({
-			wrapper: this.$page.find('.layout-side-section'),
-			css_class: 'hub-form-sidebar'
-		});
-	}
-
-	setup_filter_area() { }
-
-	setup_sort_selector() { }
-
-	// let category = this.quick_view.get_values().hub_category;
-	// return new Promise((resolve, reject) => {
-	// 	frappe.call({
-	// 		method: 'erpnext.hub_node.update_category',
-	// 		args: {
-	// 			hub_item_code: values.hub_item_code,
-	// 			category: category,
-	// 		},
-	// 		callback: (r) => {
-	// 			resolve();
-	// 		},
-	// 		freeze: true
-	// 	}).fail(reject);
-	// });
-
-	get_timeline() {
-		return `<div class="timeline">
-			<div class="timeline-head">
-			</div>
-			<div class="timeline-new-email">
-				<button class="btn btn-default btn-reply-email btn-xs">
-					${__("Reply")}
-				</button>
-			</div>
-			<div class="timeline-items"></div>
-		</div>`;
-	}
-
-	get_footer() {
-		return `<div class="form-footer">
-			<div class="after-save">
-				<div class="form-comments"></div>
-			</div>
-			<div class="pull-right scroll-to-top">
-				<a onclick="frappe.utils.scroll_to(0)"><i class="fa fa-chevron-up text-muted"></i></a>
-			</div>
-		</div>`;
-	}
-
-	get_args() {
-		return {
-			hub_sync_id: this.unique_id,
-			doctype: 'Hub ' + this.doctype
-		};
-	}
-
-	prepare_data(r) {
-		this.data = r.message;
-	}
-
-	update_data(r) {
-		this.data = r.message;
-	}
-
-	render() {
-		const image_html = this.data[this.image_field_name] ?
-			`<img src="${this.data[this.image_field_name]}">
-			<span class="helper"></span>` :
-			`<div class="standard-image">${frappe.get_abbr(this.page_title)}</div>`;
-
-		this.sidebar.remove_item('image');
-		this.sidebar.add_item({
-			name: 'image',
-			label: image_html
-		});
-
-		if(!this.form) {
-			let fields = this.formFields;
-			this.form = new frappe.ui.FieldGroup({
-				parent: this.$result,
-				fields
-			});
-			this.form.make();
-		}
-
-		if(this.data.hub_category) {
-			this.form.fields_dict.set_category.hide();
-		}
-
-		this.form.set_values(this.data);
-		this.$result.show();
-
-		this.$timelineList && this.$timelineList.empty();
-		if(this.data.reviews && this.data.reviews.length) {
-			this.data.reviews.map(review => {
-				this.addReviewToTimeline(review);
-			})
-		}
-
-		this.postRender()
-	}
-
-	postRender() {}
-
-	attachFooter() {
-		let footerHtml = `<div class="form-footer">
-			<div class="form-comments"></div>
-			<div class="pull-right scroll-to-top">
-				<a onclick="frappe.utils.scroll_to(0)"><i class="fa fa-chevron-up text-muted"></i></a>
-			</div>
-		</div>`;
-
-		let parent = $('<div>').appendTo(this.page.main.parent());
-		this.$footer = $(footerHtml).appendTo(parent);
-	}
-
-	attachTimeline() {
-		let timelineHtml = `<div class="timeline">
-			<div class="timeline-head">
-			</div>
-			<div class="timeline-new-email">
-				<button class="btn btn-default btn-reply-email btn-xs">
-					${ __("Reply") }
-				</button>
-			</div>
-			<div class="timeline-items"></div>
-		</div>`;
-
-		let parent = this.$footer.find(".form-comments");
-		this.$timeline = $(timelineHtml).appendTo(parent);
-
-		this.$timelineList = this.$timeline.find(".timeline-items");
-	}
-
-	attachReviewArea() {
-		this.comment_area = new frappe.ui.ReviewArea({
-			parent: this.$footer.find('.timeline-head'),
-			mentions: [],
-			on_submit: (val) => {
-				val.user = frappe.session.user;
-				val.username = frappe.session.user_fullname;
-				frappe.call({
-					method: 'erpnext.hub_node.send_review',
-					args: {
-						hub_item_code: this.data.hub_item_code,
-						review: val
-					},
-					callback: (r) => {
-						this.refresh();
-						this.comment_area.reset();
-					},
-					freeze: true
-				});
-			}
-		});
-	}
-
-	addReviewToTimeline(data) {
-		let username = data.username || data.user || __("Anonymous")
-		let imageHtml = data.user_image
-			? `<div class="avatar-frame" style="background-image: url(${data.user_image})"></div>`
-			: `<div class="standard-image" style="background-color: #fafbfc">${frappe.get_abbr(username)}</div>`
-
-		let editHtml = data.own
-			? `<div class="pull-right hidden-xs close-btn-container">
-				<span class="small text-muted">
-					${'data.delete'}
-				</span>
-			</div>
-			<div class="pull-right edit-btn-container">
-				<span class="small text-muted">
-					${'data.edit'}
-				</span>
-			</div>`
-			: '';
-
-		let ratingHtml = '';
-
-		for(var i = 0; i < 5; i++) {
-			let starIcon = 'fa-star-o'
-			if(i < data.rating) {
-				starIcon = 'fa-star';
-			}
-			ratingHtml += `<i class="fa fa-fw ${starIcon} star-icon" data-idx='${i}'></i>`;
-		}
-
-		$(this.getTimelineItem(data, imageHtml, editHtml, ratingHtml))
-			.appendTo(this.$timelineList);
-	}
-
-	getTimelineItem(data, imageHtml, editHtml, ratingHtml) {
-		return `<div class="media timeline-item user-content" data-doctype="${''}" data-name="${''}">
-			<span class="pull-left avatar avatar-medium hidden-xs" style="margin-top: 1px">
-				${imageHtml}
-			</span>
-
-			<div class="pull-left media-body">
-				<div class="media-content-wrapper">
-					<div class="action-btns">${editHtml}</div>
-
-					<div class="comment-header clearfix small ${'linksActive'}">
-						<span class="pull-left avatar avatar-small visible-xs">
-							${imageHtml}
-						</span>
-
-						<div class="asset-details">
-							<span class="author-wrap">
-								<i class="octicon octicon-quote hidden-xs fa-fw"></i>
-								<span>${data.username}</span>
-							</span>
-								<a href="#Form/${''}" class="text-muted">
-									<span class="text-muted hidden-xs">&ndash;</span>
-									<span class="indicator-right ${'green'}
-										delivery-status-indicator">
-										<span class="hidden-xs">${data.pretty_date}</span>
-									</span>
-								</a>
-
-								<a class="text-muted reply-link pull-right timeline-content-show"
-								title="${__('Reply')}"> ${''} </a>
-							<span class="comment-likes hidden-xs">
-								<i class="octicon octicon-heart like-action text-extra-muted not-liked fa-fw">
-								</i>
-								<span class="likes-count text-muted">10</span>
-							</span>
-						</div>
-					</div>
-					<div class="reply timeline-content-show">
-						<div class="timeline-item-content">
-								<p class="text-muted small">
-									<b>${data.subject}</b>
-								</p>
-
-								<hr>
-
-								<p class="text-muted small">
-									${ratingHtml}
-								</p>
-
-								<hr>
-								<p>
-									${data.content}
-								</p>
-						</div>
-					</div>
-				</div>
-			</div>
-		</div>`;
-	}
-
-	prepareFormFields(fields, fieldnames) {
-		return fields
-		.filter(field => fieldnames.includes(field.fieldname))
-		.map(field => {
-			let {
-				label,
-				fieldname,
-				fieldtype,
-			} = field;
-			let read_only = 1;
-			return {
-				label,
-				fieldname,
-				fieldtype,
-				read_only,
-			};
-		});
-	}
-};
-
-erpnext.hub.ItemPage = class ItemPage extends erpnext.hub.HubDetailsPage {
-	constructor(opts) {
-		super(opts);
-
-		this.show();
-	}
-
-	setup_defaults() {
-		super.setup_defaults();
-		this.doctype = 'Item';
-		this.image_field_name = 'image';
-	}
-
-	setup_page_head() {
-		super.setup_page_head();
-		this.set_primary_action();
-	}
-
-	setup_side_bar() {
-		super.setup_side_bar();
-		this.attachFooter();
-		this.attachTimeline();
-		this.attachReviewArea();
-	}
-
-	set_primary_action() {
-		let item = this.data;
-		this.page.set_primary_action(__('Request a Quote'), () => {
-			this.show_rfq_modal()
-				.then(values => {
-					item.item_code = values.item_code;
-					delete values.item_code;
-
-					const supplier = values;
-					return [item, supplier];
-				})
-				.then(([item, supplier]) => {
-					return this.make_rfq(item, supplier, this.page.btn_primary);
-				})
-				.then(r => {
-					console.log(r);
-					if (r.message && r.message.rfq) {
-						this.page.btn_primary.addClass('disabled').html(`<span><i class='fa fa-check'></i> ${__('Quote Requested')}</span>`);
-					} else {
-						throw r;
-					}
-				})
-				.catch((e) => {
-					console.log(e); //eslint-disable-line
-				});
-		}, 'octicon octicon-plus');
-	}
-
-	prepare_data(r) {
-		super.prepare_data(r);
-		this.page.set_title(this.data["item_name"]);
-	}
-
-	make_rfq(item, supplier, btn) {
-		console.log(supplier);
-		return new Promise((resolve, reject) => {
-			frappe.call({
-				method: 'erpnext.hub_node.make_rfq_and_send_opportunity',
-				args: { item, supplier },
-				callback: resolve,
-				btn,
-			}).fail(reject);
-		});
-	}
-
-	postRender() {
-		this.categoryDialog = new frappe.ui.Dialog({
-			title: __('Suggest Category'),
-			fields: [
-				{
-					label: __('Category'),
-					fieldname: 'category',
-					fieldtype: 'Autocomplete',
-					options: this.categories,
-					reqd: 1
-				}
-			],
-			primary_action_label: __("Send"),
-			primary_action: () => {
-				let values = this.categoryDialog.get_values();
-				frappe.call({
-					method: 'erpnext.hub_node.update_category',
-					args: {
-						hub_item_code: this.data.hub_item_code,
-						category: values.category
-					},
-					callback: () => {
-						this.categoryDialog.hide();
-						this.refresh();
-					},
-					freeze: true
-				}).fail(() => {});
-			}
-		});
-	}
-
-	getFormFields() {
-		let colOneFieldnames = ['item_name', 'item_code', 'description'];
-		let colTwoFieldnames = ['seller', 'company_name', 'country'];
-		let colOneFields = this.prepareFormFields(this.meta.fields, colOneFieldnames);
-		let colTwoFields = this.prepareFormFields(this.meta.fields, colTwoFieldnames);
-
-		let miscFields = [
-			{
-				label: __('Category'),
-				fieldname: 'hub_category',
-				fieldtype: 'Data',
-				read_only: 1
-			},
-
-			{
-				label: __('Suggest Category?'),
-				fieldname: 'set_category',
-				fieldtype: 'Button',
-				click: () => {
-					this.categoryDialog.show();
-				}
-			},
-
-			{
-				fieldname: 'cb1',
-				fieldtype: 'Column Break'
-			}
-		];
-		this.formFields = colOneFields.concat(miscFields, colTwoFields);
-	}
-
-	show_rfq_modal() {
-		let item = this.data;
-		return new Promise(res => {
-			let fields = [
-				{ label: __('Item Code'), fieldtype: 'Data', fieldname: 'item_code', default: item.item_code },
-				{ fieldtype: 'Column Break' },
-				{ label: __('Item Group'), fieldtype: 'Link', fieldname: 'item_group', default: item.item_group },
-				{ label: __('Supplier Details'), fieldtype: 'Section Break' },
-				{ label: __('Supplier Name'), fieldtype: 'Data', fieldname: 'supplier_name', default: item.company_name },
-				{ label: __('Supplier Email'), fieldtype: 'Data', fieldname: 'supplier_email', default: item.seller },
-				{ fieldtype: 'Column Break' },
-				{ label: __('Supplier Group'), fieldname: 'supplier_group',
-					fieldtype: 'Link', options: 'Supplier Group' }
-			];
-			fields = fields.map(f => { f.reqd = 1; return f; });
-
-			const d = new frappe.ui.Dialog({
-				title: __('Request for Quotation'),
-				fields: fields,
-				primary_action_label: __('Send'),
-				primary_action: (values) => {
-					res(values);
-					d.hide();
-				}
-			});
-
-			d.show();
-		});
-	}
-}
-
-erpnext.hub.CompanyPage = class CompanyPage extends erpnext.hub.HubDetailsPage {
-	constructor(opts) {
-		super(opts);
-		this.show();
-	}
-
-	setup_defaults() {
-		super.setup_defaults();
-		this.doctype = 'Company';
-		this.image_field_name = 'company_logo';
-	}
-
-	prepare_data(r) {
-		super.prepare_data(r);
-		this.page.set_title(this.data["company_name"]);
-	}
-
-	getFormFields() {
-		let fieldnames = ['company_name', 'description', 'route', 'country', 'seller', 'site_name'];;
-		this.formFields = this.prepareFormFields(this.meta.fields, fieldnames);
-	}
-}
diff --git a/erpnext/public/js/hub/hub_listing.js b/erpnext/public/js/hub/hub_listing.js
deleted file mode 100644
index 368c723..0000000
--- a/erpnext/public/js/hub/hub_listing.js
+++ /dev/null
@@ -1,802 +0,0 @@
-
-erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
-	setup_defaults() {
-		super.setup_defaults();
-		this.page_title = __('');
-		this.method = 'erpnext.hub_node.get_list';
-
-		this.cache = {};
-
-		const route = frappe.get_route();
-		this.page_name = route[1];
-
-		this.menu_items = this.menu_items.concat(this.get_menu_items());
-
-		this.imageFieldName = 'image';
-
-		this.show_filters = 0;
-	}
-
-	set_title() {
-		const title = this.page_title;
-		let iconHtml = `<img class="hub-icon" src="assets/erpnext/images/hub_logo.svg">`;
-		let titleHtml = `<span class="hub-page-title">${title}</span>`;
-		this.page.set_title(titleHtml, '', false, title);
-	}
-
-	setup_fields() {
-		return this.get_meta()
-			.then(r => {
-				this.meta = r.message.meta || this.meta;
-				frappe.model.sync(this.meta);
-				this.bootstrap_data(r.message);
-
-				this.prepareFormFields();
-			});
-	}
-
-	setup_filter_area() { }
-
-	get_meta() {
-		return new Promise(resolve =>
-			frappe.call('erpnext.hub_node.get_meta', { doctype: this.doctype }, resolve));
-	}
-
-	set_breadcrumbs() { }
-
-	prepareFormFields() { }
-
-	bootstrap_data() { }
-
-	get_menu_items() {
-		const items = [
-			{
-				label: __('Hub Settings'),
-				action: () => frappe.set_route('Form', 'Hub Settings'),
-				standard: true
-			},
-			{
-				label: __('Favourites'),
-				action: () => frappe.set_route('Hub', 'Favourites'),
-				standard: true
-			}
-		];
-
-		return items;
-	}
-
-	setup_side_bar() {
-		this.sidebar = new frappe.ui.Sidebar({
-			wrapper: this.page.wrapper.find('.layout-side-section'),
-			css_class: 'hub-sidebar'
-		});
-	}
-
-	setup_sort_selector() {
-		// this.sort_selector = new frappe.ui.SortSelector({
-		// 	parent: this.filter_area.$filter_list_wrapper,
-		// 	doctype: this.doctype,
-		// 	args: this.order_by,
-		// 	onchange: () => this.refresh(true)
-		// });
-	}
-
-	setup_view() {
-		if (frappe.route_options) {
-			const filters = [];
-			for (let field in frappe.route_options) {
-				var value = frappe.route_options[field];
-				this.page.fields_dict[field].set_value(value);
-			}
-		}
-
-		const $hub_search = $(`
-			<div class="hub-search-container">
-				<input type="text" class="form-control" placeholder="Search for anything">
-			</div>`
-		);
-		this.$frappe_list.prepend($hub_search);
-		const $search_input = $hub_search.find('input');
-
-		$search_input.on('keydown', frappe.utils.debounce((e) => {
-			if (e.which === frappe.ui.keyCode.ENTER) {
-				this.search_value = $search_input.val();
-				this.refresh();
-			}
-		}, 300));
-	}
-
-	get_args() {
-		return {
-			doctype: this.doctype,
-			start: this.start,
-			limit: this.page_length,
-			order_by: this.order_by,
-			// fields: this.fields,
-			filters: this.get_filters_for_args()
-		};
-	}
-
-	update_data(r) {
-		const data = r.message;
-
-		if (this.start === 0) {
-			this.data = data;
-		} else {
-			this.data = this.data.concat(data);
-		}
-
-		this.data_dict = {};
-	}
-
-	freeze(toggle) { }
-
-	render() {
-		this.data_dict = {};
-		this.render_image_view();
-
-		this.setup_quick_view();
-		this.setup_like();
-	}
-
-	render_offline_card() {
-		let html = `<div class='page-card'>
-			<div class='page-card-head'>
-				<span class='indicator red'>
-					{{ _("Payment Cancelled") }}</span>
-			</div>
-			<p>${ __("Your payment is cancelled.")}</p>
-			<div><a href='' class='btn btn-primary btn-sm'>
-				${ __("Continue")}</a></div>
-		</div>`;
-
-		let page = this.page.wrapper.find('.layout-side-section')
-		page.append(html);
-
-		return;
-	}
-
-	render_image_view() {
-		var html = this.data.map(this.item_html.bind(this)).join("");
-
-		if (this.start === 0) {
-			// ${this.getHeaderHtml()}
-			this.$result.html(`
-				<div class="row hub-card-container">
-					<div class="col-md-12 margin-bottom">
-						<b>Recently Published</b>
-					</div>
-					${html}
-				</div>
-			`);
-		}
-
-		if (this.data.length) {
-			this.doc = this.data[0];
-		}
-
-		this.data.map(this.loadImage.bind(this));
-
-		this.data_dict = {};
-		this.data.map(d => {
-			this.data_dict[d.hub_item_code] = d;
-		});
-	}
-
-	getHeaderHtml(title, image, content) {
-		// let company_html =
-		return `
-			<header class="list-row-head text-muted small">
-				<div style="display: flex;">
-					<div class="list-header-icon">
-						<img title="${title}" alt="${title}" src="${image}">
-					</div>
-					<div class="list-header-info">
-						<h5>
-							${title}
-						</h5>
-						<span class="margin-vertical-10 level-item">
-							${content}
-						</span>
-					</div>
-				</div>
-			</header>
-		`;
-	}
-
-	renderHeader() {
-		return ``;
-	}
-
-	get_image_html(encoded_name, src, alt_text) {
-		return `<img data-name="${encoded_name}" src="${src}" alt="${alt_text}">`;
-	}
-
-	get_image_placeholder(title) {
-		return `<span class="placeholder-text">${frappe.get_abbr(title)}</span>`;
-	}
-
-	loadImage(item) {
-		item._name = encodeURI(item.name);
-		const encoded_name = item._name;
-		const title = strip_html(item[this.meta.title_field || 'name']);
-
-		let placeholder = this.get_image_placeholder(title);
-		let $container = this.$result.find(`.image-field[data-name="${encoded_name}"]`);
-
-		if (!item[this.imageFieldName]) {
-			$container.prepend(placeholder);
-			$container.addClass('no-image');
-		}
-
-		frappe.load_image(item[this.imageFieldName],
-			(imageObj) => {
-				$container.prepend(imageObj)
-			},
-			() => {
-				$container.prepend(placeholder);
-				$container.addClass('no-image');
-			},
-			(imageObj) => {
-				imageObj.title = encoded_name;
-				imageObj.alt = title;
-			}
-		)
-	}
-
-	setup_quick_view() {
-		if (this.quick_view) return;
-
-		this.quick_view = new frappe.ui.Dialog({
-			title: 'Quick View',
-			fields: this.formFields
-		});
-		this.quick_view.set_primary_action(__('Request a Quote'), () => {
-			this.show_rfq_modal()
-				.then(values => {
-					item.item_code = values.item_code;
-					delete values.item_code;
-
-					const supplier = values;
-					return [item, supplier];
-				})
-				.then(([item, supplier]) => {
-					return this.make_rfq(item, supplier, this.page.btn_primary);
-				})
-				.then(r => {
-					console.log(r);
-					if (r.message && r.message.rfq) {
-						this.page.btn_primary.addClass('disabled').html(`<span><i class='fa fa-check'></i> ${__('Quote Requested')}</span>`);
-					} else {
-						throw r;
-					}
-				})
-				.catch((e) => {
-					console.log(e); //eslint-disable-line
-				});
-		}, 'octicon octicon-plus');
-
-		this.$result.on('click', '.btn.zoom-view', (e) => {
-			e.preventDefault();
-			e.stopPropagation();
-			var name = $(e.target).attr('data-name');
-			name = decodeURIComponent(name);
-
-			this.quick_view.set_title(name);
-			let values = this.data_dict[name];
-			this.quick_view.set_values(values);
-
-			let fields = [];
-
-			this.quick_view.show();
-
-			return false;
-		});
-	}
-
-	setup_like() {
-		if (this.setup_like_done) return;
-		this.setup_like_done = 1;
-		this.$result.on('click', '.btn.like-button', (e) => {
-			if ($(e.target).hasClass('changing')) return;
-			$(e.target).addClass('changing');
-
-			e.preventDefault();
-			e.stopPropagation();
-
-			var name = $(e.target).attr('data-name');
-			name = decodeURIComponent(name);
-			let values = this.data_dict[name];
-
-			let heart = $(e.target);
-			if (heart.hasClass('like-button')) {
-				heart = $(e.target).find('.octicon');
-			}
-
-			let remove = 1;
-
-			if (heart.hasClass('liked')) {
-				// unlike
-				heart.removeClass('liked');
-			} else {
-				// like
-				remove = 0;
-				heart.addClass('liked');
-			}
-
-			frappe.call({
-				method: 'erpnext.hub_node.update_wishlist_item',
-				args: {
-					item_name: values.hub_item_code,
-					remove: remove
-				},
-				callback: (r) => {
-					let message = __("Added to Favourites");
-					if (remove) {
-						message = __("Removed from Favourites");
-					}
-					frappe.show_alert(message);
-				},
-				freeze: true
-			});
-
-			$(e.target).removeClass('changing');
-			return false;
-		});
-	}
-}
-
-erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing {
-	constructor(opts) {
-		super(opts);
-		this.show();
-	}
-
-	setup_defaults() {
-		super.setup_defaults();
-		this.doctype = 'Hub Item';
-		this.page_title = __('Marketplace');
-		this.fields = ['name', 'hub_item_code', 'image', 'item_name', 'item_code', 'company_name', 'description', 'country'];
-		this.filters = [];
-	}
-
-	render() {
-		this.data_dict = {};
-		this.render_image_view();
-
-		this.setup_quick_view();
-		this.setup_like();
-	}
-
-	bootstrap_data(response) {
-		// let companies = response.companies.map(d => d.name);
-		// this.custom_filter_configs = [
-		// 	{
-		// 		fieldtype: 'Autocomplete',
-		// 		label: __('Select Company'),
-		// 		condition: 'like',
-		// 		fieldname: 'company_name',
-		// 		options: companies
-		// 	},
-		// 	{
-		// 		fieldtype: 'Link',
-		// 		label: __('Select Country'),
-		// 		options: 'Country',
-		// 		condition: 'like',
-		// 		fieldname: 'country'
-		// 	}
-		// ];
-	}
-
-	prepareFormFields() {
-		let fieldnames = ['item_name', 'description', 'company_name', 'country'];
-		this.formFields = this.meta.fields
-			.filter(field => fieldnames.includes(field.fieldname))
-			.map(field => {
-				let {
-					label,
-					fieldname,
-					fieldtype,
-				} = field;
-				let read_only = 1;
-				return {
-					label,
-					fieldname,
-					fieldtype,
-					read_only,
-				};
-			});
-
-		this.formFields.unshift({
-			label: 'image',
-			fieldname: 'image',
-			fieldtype: 'Attach Image'
-		});
-	}
-
-	setup_side_bar() {
-		super.setup_side_bar();
-
-		this.setup_new_sidebar();
-
-		return;
-
-		let $pitch = $(`<div class="border" style="
-				margin-top: 10px;
-				padding: 0px 10px;
-				border-radius: 3px;
-			">
-			<h5>Sell on HubMarket</h5>
-			<p>Over 2000 products listed. Register your company to start selling.</p>
-		</div>`);
-
-		this.sidebar.$sidebar.append($pitch);
-
-		this.category_tree = new frappe.ui.Tree({
-			parent: this.sidebar.$sidebar,
-			label: 'All Categories',
-			expandable: true,
-
-			args: { parent: this.current_category },
-			method: 'erpnext.hub_node.get_categories',
-			on_click: (node) => {
-				this.update_category(node.label);
-			}
-		});
-
-		this.sidebar.add_item({
-			label: __('Companies'),
-			on_click: () => frappe.set_route('Hub', 'Company')
-		}, undefined, true);
-
-		this.sidebar.add_item({
-			label: this.hub_settings.company,
-			on_click: () => frappe.set_route('Form', 'Company', this.hub_settings.company)
-		}, __("Account"));
-
-		this.sidebar.add_item({
-			label: __("Favourites"),
-			on_click: () => frappe.set_route('Hub', 'Favourites')
-		}, __("Account"));
-
-		this.sidebar.add_item({
-			label: __("Settings"),
-			on_click: () => frappe.set_route('Form', 'Hub Settings')
-		}, __("Account"));
-	}
-
-	setup_new_sidebar() {
-		this.sidebar.$sidebar.append(`
-			<ul class="list-unstyled hub-sidebar-group">
-				<li class="hub-sidebar-item bold active">
-					Browse
-				</li>
-				<li class="hub-sidebar-item text-muted">
-					Favorites
-				</li>
-				<li class="hub-sidebar-item text-muted">
-					Become a seller
-				</li>
-			</ul>
-		`);
-
-		frappe.call('erpnext.hub_node.get_categories')
-			.then(r => {
-				const categories = r.message.map(d => d.value).sort();
-				const sidebar_items = [
-					`<li class="hub-sidebar-item bold text-muted is-title">
-						${__('Category')}
-					</li>`,
-					`<li class="hub-sidebar-item active">
-						All
-					</li>`,
-					...categories.map(category => `
-						<li class="hub-sidebar-item text-muted">
-							${category}
-						</li>
-					`)
-				];
-
-				this.sidebar.$sidebar.append(`
-					<ul class="list-unstyled">
-						${sidebar_items.join('')}
-					</ul>
-				`);
-			});
-	}
-
-	update_category(label) {
-		this.current_category = (label == 'All Categories') ? undefined : label;
-		this.refresh();
-	}
-
-	get_filters_for_args() {
-		const filter = {};
-
-		if (this.search_value) {
-			filter.item_name = ['like', `%${this.search_value}%`];
-		}
-
-		filter.image = ['like', 'http%'];
-		return filter;
-
-		// if(!this.filter_area) return;
-		// let filters = {};
-		// this.filter_area.get().forEach(f => {
-		// 	let field = f[1] !== 'name' ? f[1] : 'item_name';
-		// 	filters[field] = [f[2], f[3]];
-		// });
-		// if(this.current_category) {
-		// 	filters['hub_category'] = this.current_category;
-		// }
-		// return filters;
-	}
-
-	update_data(r) {
-		super.update_data(r);
-
-		this.data_dict = {};
-		this.data.map(d => {
-			this.data_dict[d.hub_item_code] = d;
-		});
-	}
-
-	item_html(item, index) {
-		item._name = encodeURI(item.name);
-		const encoded_name = item._name;
-		const title = strip_html(item[this.meta.title_field || 'name']);
-
-		const img_url = item[this.imageFieldName];
-		const no_image = !img_url;
-		const _class = no_image ? 'no-image' : '';
-		const route = `#Hub/Item/${item.hub_item_code}`;
-		const company_name = item['company_name'];
-
-		const reviewLength = (item.reviews || []).length;
-		const ratingAverage = reviewLength
-			? item.reviews
-				.map(r => r.rating)
-				.reduce((a, b) => a + b, 0) / reviewLength
-			: -1;
-
-		let ratingHtml = ``;
-
-		for (var i = 0; i < 5; i++) {
-			let starClass = 'fa-star';
-			if (i >= ratingAverage) starClass = 'fa-star-o';
-			ratingHtml += `<i class='fa fa-fw ${starClass} star-icon' data-index=${i}></i>`;
-		}
-		let dot_spacer = '<span aria-hidden="true"> · </span>';
-		let subtitle = '';
-		subtitle += comment_when(item.creation);
-		subtitle += dot_spacer;
-
-		if (ratingAverage > 0) {
-			subtitle += ratingAverage + `<i class='fa fa-fw fa-star-o'></i>`;
-			subtitle += dot_spacer;
-		}
-		subtitle += company_name;
-
-		let item_html = `
-			<div class="col-sm-3 col-xs-2">
-				<div class="hub-card">
-					<div class="hub-card-header">
-						<div class="list-row-col list-subject ellipsis level">
-							<span class="level-item bold ellipsis" title="McGuffin">
-								<a href="${route}">${title}</a>
-							</span>
-						</div>
-						<div class="text-muted small" style="margin: 5px 0px;">
-							${ratingHtml}
-							(${reviewLength})
-						</div>
-						<div class="list-row-col">
-							<a href="${'#Hub/Company/' + company_name + '/Items'}"><p>${company_name}</p></a>
-						</div>
-					</div>
-					<div class="hub-card-body">
-						<a  data-name="${encoded_name}"
-							title="${encoded_name}"
-							href="${route}"
-						>
-							<div class="image-field ${_class}"
-								data-name="${encoded_name}"
-							>
-								<button class="btn btn-default zoom-view" data-name="${encoded_name}">
-									<i class="octicon octicon-eye" data-name="${encoded_name}"></i>
-								</button>
-								<button class="btn btn-default like-button" data-name="${encoded_name}">
-									<i class="octicon octicon-heart" data-name="${encoded_name}"></i>
-								</button>
-							</div>
-						</a>
-					</div>
-				</div>
-			</div>
-		`;
-
-		item_html = `
-			<div class="col-md-3 col-sm-4 col-xs-6">
-				<div class="hub-card">
-					<div class="hub-card-header">
-						<div class="hub-card-title ellipsis bold">${title}</div>
-						<div class="hub-card-subtitle ellipsis text-muted">${subtitle}</div>
-					</div>
-					<div class="hub-card-body">
-						<img class="hub-card-image ${no_image ? 'no-image' : ''}" src="${img_url}" />
-					</div>
-				</div>
-			</div>
-		`;
-
-		return item_html;
-	}
-
-};
-
-erpnext.hub.Favourites2 = class Favourites extends erpnext.hub.ItemListing {
-	constructor(opts) {
-		super(opts);
-		this.show();
-	}
-
-	setup_defaults() {
-		super.setup_defaults();
-		this.doctype = 'Hub Item';
-		this.page_title = __('Favourites');
-		this.fields = ['name', 'hub_item_code', 'image', 'item_name', 'item_code', 'company_name', 'description', 'country'];
-		this.filters = [];
-		this.method = 'erpnext.hub_node.get_item_favourites';
-	}
-
-	setup_filter_area() { }
-
-	setup_sort_selector() { }
-
-	// setupHe
-
-	getHeaderHtml() {
-		return '';
-	}
-
-	get_args() {
-		return {
-			start: this.start,
-			limit: this.page_length,
-			order_by: this.order_by,
-			fields: this.fields
-		};
-	}
-
-	bootstrap_data(response) { }
-
-	prepareFormFields() { }
-
-	setup_side_bar() {
-		this.sidebar = new frappe.ui.Sidebar({
-			wrapper: this.page.wrapper.find('.layout-side-section'),
-			css_class: 'hub-sidebar'
-		});
-
-		this.sidebar.add_item({
-			label: __('Back to Products'),
-			on_click: () => frappe.set_route('Hub', 'Item')
-		});
-	}
-
-	update_category(label) {
-		this.current_category = (label == 'All Categories') ? undefined : label;
-		this.refresh();
-	}
-
-	get_filters_for_args() {
-		if (!this.filter_area) return;
-		let filters = {};
-		this.filter_area.get().forEach(f => {
-			let field = f[1] !== 'name' ? f[1] : 'item_name';
-			filters[field] = [f[2], f[3]];
-		});
-		if (this.current_category) {
-			filters['hub_category'] = this.current_category;
-		}
-		return filters;
-	}
-
-	update_data(r) {
-		super.update_data(r);
-
-		this.data_dict = {};
-		this.data.map(d => {
-			this.data_dict[d.hub_item_code] = d;
-		});
-	}
-};
-
-erpnext.hub.CompanyListing = class CompanyListing extends erpnext.hub.HubListing {
-	constructor(opts) {
-		super(opts);
-		this.show();
-	}
-
-	render() {
-		this.data_dict = {};
-		this.render_image_view();
-	}
-
-	setup_defaults() {
-		super.setup_defaults();
-		this.doctype = 'Hub Company';
-		this.page_title = __('Companies');
-		this.fields = ['company_logo', 'name', 'site_name', 'seller_city', 'seller_description', 'seller', 'country', 'company_name'];
-		this.filters = [];
-		this.custom_filter_configs = [
-			{
-				fieldtype: 'Link',
-				label: 'Country',
-				options: 'Country',
-				condition: 'like',
-				fieldname: 'country'
-			}
-		];
-		this.imageFieldName = 'company_logo';
-	}
-
-	setup_side_bar() {
-		this.sidebar = new frappe.ui.Sidebar({
-			wrapper: this.page.wrapper.find('.layout-side-section'),
-			css_class: 'hub-sidebar'
-		});
-
-		this.sidebar.add_item({
-			label: __('Back to Products'),
-			on_click: () => frappe.set_route('Hub', 'Item')
-		});
-	}
-
-	get_filters_for_args() {
-		let filters = {};
-		// this.filter_area.get().forEach(f => {
-		// 	let field = f[1] !== 'name' ? f[1] : 'company_name';
-		// 	filters[field] = [f[2], f[3]];
-		// });
-		return filters;
-	}
-
-	item_html(company) {
-		company._name = encodeURI(company.company_name);
-		const encoded_name = company._name;
-		const title = strip_html(company.company_name);
-		const _class = !company[this.imageFieldName] ? 'no-image' : '';
-		const company_name = company['company_name'];
-		const route = `#Hub/Company/${company_name}`;
-
-		let image_html = company.company_logo ?
-			`<img src="${company.company_logo}"><span class="helper"></span>` :
-			`<div class="standard-image">${frappe.get_abbr(company.company_name)}</div>`;
-
-		let item_html = `
-			<div class="image-view-item">
-				<div class="image-view-header">
-					<div class="list-row-col list-subject ellipsis level">
-						<span class="level-item bold ellipsis" title="McGuffin">
-							<a href="${route}">${title}</a>
-						</span>
-					</div>
-				</div>
-				<div class="image-view-body">
-					<a  data-name="${encoded_name}"
-						title="${encoded_name}"
-						href="${route}">
-						<div class="image-field ${_class}"
-							data-name="${encoded_name}">
-						</div>
-					</a>
-				</div>
-
-			</div>
-		`;
-
-		return item_html;
-	}
-
-};
diff --git a/erpnext/public/js/hub/marketplace.js b/erpnext/public/js/hub/marketplace.js
index 31cf5da..ab0d4f7 100644
--- a/erpnext/public/js/hub/marketplace.js
+++ b/erpnext/public/js/hub/marketplace.js
@@ -1,3 +1,19 @@
+// pages
+import './pages/home';
+import './pages/favourites';
+import './pages/search';
+import './pages/category';
+import './pages/item';
+import './pages/register';
+import './pages/profile';
+import './pages/publish';
+import './pages/published_products';
+import './pages/not_found';
+
+// helpers
+import './helpers';
+import './hub_call';
+
 frappe.provide('hub');
 frappe.provide('erpnext.hub');
 
@@ -180,1119 +196,3 @@
 		this.subpages[route[1]].show();
 	}
 }
-
-class SubPage {
-	constructor(parent, options) {
-		this.$parent = $(parent);
-		this.make_wrapper(options);
-
-		// handle broken images after every render
-		if (this.render) {
-			this._render = this.render.bind(this);
-
-			this.render = (...args) => {
-				this._render(...args);
-				frappe.dom.handle_broken_images(this.$wrapper);
-			}
-		}
-	}
-
-	make_wrapper() {
-		const page_name = frappe.get_route()[1];
-		this.$wrapper = $(`<div class="marketplace-page" data-page-name="${page_name}">`).appendTo(this.$parent);
-		this.hide();
-	}
-
-	empty() {
-		this.$wrapper.empty();
-	}
-
-	show() {
-		this.refresh();
-		this.$wrapper.show();
-	}
-
-	hide() {
-		this.$wrapper.hide();
-	}
-}
-
-erpnext.hub.Home = class Home extends SubPage {
-	make_wrapper() {
-		super.make_wrapper();
-
-		make_search_bar({
-			wrapper: this.$wrapper,
-			on_search: keyword => {
-				frappe.set_route('marketplace', 'search', keyword);
-			}
-		});
-	}
-
-	refresh() {
-		this.get_items_and_render();
-	}
-
-	get_items_and_render() {
-		this.$wrapper.find('.hub-card-container').empty();
-		this.get_data()
-			.then(data => {
-				this.render(data);
-			});
-	}
-
-	get_data() {
-		return hub.call('get_data_for_homepage', { country: frappe.defaults.get_user_default('country') });
-	}
-
-	render(data) {
-		let html = get_item_card_container_html(data.random_items, __('Explore'));
-		this.$wrapper.append(html);
-
-		if (data.items_by_country.length) {
-			html = get_item_card_container_html(data.items_by_country, __('Near you'));
-			this.$wrapper.append(html);
-		}
-	}
-}
-
-erpnext.hub.Favourites = class Favourites extends SubPage {
-	refresh() {
-		this.get_favourites()
-			.then(items => {
-				this.render(items);
-			});
-	}
-
-	get_favourites() {
-		return hub.call('get_item_favourites');
-	}
-
-	render(items) {
-		this.$wrapper.find('.hub-card-container').empty();
-		const html = get_item_card_container_html(items, __('Favourites'));
-		this.$wrapper.append(html)
-	}
-}
-
-erpnext.hub.Category = class Category extends SubPage {
-	refresh() {
-		this.category = frappe.get_route()[2];
-		this.get_items_for_category(this.category)
-			.then(r => {
-				this.render(r.message);
-			});
-	}
-
-	get_items_for_category(category) {
-		this.$wrapper.find('.hub-card-container').empty();
-		return frappe.call('erpnext.hub_node.get_list', {
-			doctype: 'Hub Item',
-			filters: {
-				hub_category: category
-			}
-		});
-	}
-
-	render(items) {
-		const html = get_item_card_container_html(items, __(this.category));
-		this.$wrapper.append(html)
-	}
-}
-
-erpnext.hub.SearchPage = class SearchPage extends SubPage {
-	make_wrapper() {
-		super.make_wrapper();
-
-		make_search_bar({
-			wrapper: this.$wrapper,
-			on_search: keyword => {
-				frappe.set_route('marketplace', 'search', keyword);
-			}
-		});
-	}
-
-	refresh() {
-		this.keyword = frappe.get_route()[2] || '';
-		this.$wrapper.find('input').val(this.keyword);
-
-		this.get_items_by_keyword(this.keyword)
-			.then(items => this.render(items));
-	}
-
-	get_items_by_keyword(keyword) {
-		return hub.call('get_items_by_keyword', { keyword });
-	}
-
-	render(items) {
-		this.$wrapper.find('.hub-card-container').remove();
-		const title = this.keyword ? __('Search results for "{0}"', [this.keyword]) : '';
-		const html = get_item_card_container_html(items, title);
-		this.$wrapper.append(html);
-	}
-}
-
-erpnext.hub.Item = class Item extends SubPage {
-	make_wrapper() {
-		super.make_wrapper();
-		this.setup_events();
-	}
-
-	refresh() {
-		this.show_skeleton();
-		this.hub_item_code = frappe.get_route()[2];
-
-		this.own_item = false;
-
-		this.get_item(this.hub_item_code)
-			.then(item => {
-				this.own_item = item.hub_seller === hub.settings.company_email;
-				this.item = item;
-				this.render(item);
-			});
-	}
-
-	show_skeleton() {
-		const skeleton = `<div class="hub-item-container">
-			<div class="row">
-				<div class="col-md-3">
-					<div class="hub-item-skeleton-image"></div>
-				</div>
-				<div class="col-md-6">
-					<h2 class="hub-skeleton" style="width: 75%;">Name</h2>
-					<div class="text-muted">
-						<p class="hub-skeleton" style="width: 35%;">Details</p>
-						<p class="hub-skeleton" style="width: 50%;">Ratings</p>
-					</div>
-					<hr>
-					<div class="hub-item-description">
-						<p class="hub-skeleton">Desc</p>
-						<p class="hub-skeleton" style="width: 85%;">Desc</p>
-					</div>
-				</div>
-			</div>
-		</div>`;
-
-		this.$wrapper.html(skeleton);
-	}
-
-	setup_events() {
-		this.$wrapper.on('click', '.btn-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')
-					}
-				]
-			});
-
-			d.show();
-		});
-	}
-
-	get_item(hub_item_code) {
-		return hub.call('get_item_details', { hub_item_code });
-	}
-
-	render(item) {
-		const title = item.item_name || item.name;
-		const seller = item.company;
-
-		const who = __('Posted By {0}', [seller]);
-		const when = comment_when(item.creation);
-
-		const city = item.city ? item.city + ', ' : '';
-		const country = item.country ? item.country : '';
-		const where = `${city}${country}`;
-
-		const dot_spacer = '<span aria-hidden="true"> · </span>';
-
-		const description = item.description || '';
-
-		const rating_html = get_rating_html(item.average_rating);
-		const rating_count = item.no_of_ratings > 0 ? `${item.no_of_ratings} reviews` : __('No reviews yet');
-
-		let edit_buttons_html = '';
-
-		if(this.own_item) {
-			edit_buttons_html = `<div style="margin-top: 20px">
-				<button class="btn btn-secondary btn-default btn-xs margin-right edit-item">Edit Details</button>
-				<button class="btn btn-secondary btn-danger btn-xs unpublish">Unpublish</button>
-			</div>`;
-		}
-
-		const html = `
-			<div class="hub-item-container">
-				<div class="row visible-xs">
-					<div class="col-xs-12 margin-bottom">
-						<button class="btn btn-xs btn-default" data-route="marketplace/home">Back to home</button>
-					</div>
-				</div>
-				<div class="row">
-					<div class="col-md-3">
-						<div class="hub-item-image">
-							<img src="${item.image}">
-						</div>
-					</div>
-					<div class="col-md-8">
-						<h2>${title}</h2>
-						<div class="text-muted">
-							<p>${where}${dot_spacer}${when}</p>
-							<p>${rating_html} (${rating_count})</p>
-						</div>
-						<hr>
-						<div class="hub-item-description">
-						${description ?
-							`<b>${__('Description')}</b>
-							<p>${description}</p>
-							` : `<p>${__('No description')}<p>`
-						}
-						</div>
-						${edit_buttons_html}
-					</div>
-					<div 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><a>Edit Details</a></li>
-								<li><a>Unpublish</a></li>
-							</ul>
-						</div>
-					</div>
-				</div>
-				<div class="row hub-item-seller">
-					<div class="col-md-12 margin-top margin-bottom">
-						<b class="text-muted">Seller Information</b>
-					</div>
-					<div class="col-md-1">
-						<img src="https://picsum.photos/200">
-					</div>
-					<div class="col-md-8">
-						<div class="margin-bottom"><a href="#marketplace/seller/${seller}" class="bold">${seller}</a></div>
-						<button class="btn btn-xs btn-default text-muted btn-contact-seller">
-							${__('Contact Seller')}
-						</button>
-					</div>
-				</div>
-				<!-- review area -->
-				<div class="row hub-item-review-container">
-					<div class="col-md-12 form-footer">
-						<div class="form-comments">
-							<div class="timeline">
-								<div class="timeline-head"></div>
-								<div class="timeline-items"></div>
-							</div>
-						</div>
-						<div class="pull-right scroll-to-top">
-							<a onclick="frappe.utils.scroll_to(0)"><i class="fa fa-chevron-up text-muted"></i></a>
-						</div>
-					</div>
-				</div>
-			</div>
-		`;
-
-		this.$wrapper.html(html);
-
-		if(this.own_item) {
-			this.bind_edit_buttons();
-		}
-
-		this.make_review_area();
-
-		this.get_reviews()
-			.then(reviews => {
-				this.reviews = reviews;
-				this.render_reviews(reviews);
-			});
-	}
-
-	bind_edit_buttons() {
-		this.edit_dialog = new frappe.ui.Dialog({
-			title: "Edit Your Product",
-			fields: []
-		});
-
-		this.$wrapper.find('.edit-item').on('click', this.on_edit.bind(this));
-		this.$wrapper.find('.unpublish').on('click', this.on_unpublish.bind(this));
-	}
-
-	on_edit() {
-		this.edit_dialog.show();
-	}
-
-	on_unpublish() {
-		if(!this.unpublish_dialog) {
-			this.unpublish_dialog = new frappe.ui.Dialog({
-				title: "Edit Your Product",
-				fields: []
-			});
-		}
-
-		this.unpublish_dialog.show();
-	}
-
-	make_review_area() {
-		this.comment_area = new frappe.ui.ReviewArea({
-			parent: this.$wrapper.find('.timeline-head').empty(),
-			mentions: [],
-			on_submit: (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(review => {
-					this.reviews = this.reviews || [];
-					this.reviews.push(review);
-					this.render_reviews(this.reviews);
-
-					this.comment_area.reset();
-				});
-			}
-		});
-	}
-
-	get_reviews() {
-		return hub.call('get_item_reviews', { hub_item_code: this.hub_item_code }).catch(() => {});
-	}
-
-	render_reviews(reviews=[]) {
-		this.$wrapper.find('.timeline-items').empty();
-
-		reviews.sort((a, b) => {
-			if (a.modified > b.modified) {
-				return -1;
-			}
-
-			if (a.modified < b.modified) {
-				return 1;
-			}
-
-			return 0;
-		});
-
-		reviews.forEach(review => this.render_review(review));
-	}
-
-	render_review(review) {
-		let username = review.username || review.user || __("Anonymous");
-
-		let image_html = review.user_image
-			? `<div class="avatar-frame" style="background-image: url(${review.user_image})"></div>`
-			: `<div class="standard-image" style="background-color: #fafbfc">${frappe.get_abbr(username)}</div>`
-
-		let edit_html = review.own
-			? `<div class="pull-right hidden-xs close-btn-container">
-				<span class="small text-muted">
-					${'data.delete'}
-				</span>
-			</div>
-			<div class="pull-right edit-btn-container">
-				<span class="small text-muted">
-					${'data.edit'}
-				</span>
-			</div>`
-			: '';
-
-		let rating_html = get_rating_html(review.rating);
-
-		const $timeline_items = this.$wrapper.find('.timeline-items');
-
-		$(this.get_timeline_item(review, image_html, edit_html, rating_html))
-			.appendTo($timeline_items);
-	}
-
-	get_timeline_item(data, image_html, edit_html, rating_html) {
-		return `<div class="media timeline-item user-content" data-doctype="${''}" data-name="${''}">
-			<span class="pull-left avatar avatar-medium hidden-xs" style="margin-top: 1px">
-				${image_html}
-			</span>
-			<div class="pull-left media-body">
-				<div class="media-content-wrapper">
-					<div class="action-btns">${edit_html}</div>
-
-					<div class="comment-header clearfix">
-						<span class="pull-left avatar avatar-small visible-xs">
-							${image_html}
-						</span>
-
-						<div class="asset-details">
-							<span class="author-wrap">
-								<i class="octicon octicon-quote hidden-xs fa-fw"></i>
-								<span>${data.username}</span>
-							</span>
-							<a class="text-muted">
-								<span class="text-muted hidden-xs">&ndash;</span>
-								<span class="hidden-xs">${comment_when(data.modified)}</span>
-							</a>
-						</div>
-					</div>
-					<div class="reply timeline-content-show">
-						<div class="timeline-item-content">
-							<p class="text-muted">
-								${rating_html}
-							</p>
-							<h6 class="bold">${data.subject}</h6>
-							<p class="text-muted">
-								${data.content}
-							</p>
-						</div>
-					</div>
-				</div>
-			</div>
-		</div>`;
-	}
-}
-erpnext.hub.Register = class Register extends SubPage {
-	make_wrapper() {
-		super.make_wrapper();
-		this.$register_container = $(`<div class="row register-container">`)
-			.appendTo(this.$wrapper);
-		this.$form_container = $('<div class="col-md-8 col-md-offset-1 form-container">')
-			.appendTo(this.$wrapper);
-	}
-
-	refresh() {
-		this.$register_container.empty();
-		this.$form_container.empty();
-		this.render();
-	}
-
-	render() {
-		this.make_field_group();
-	}
-
-	make_field_group() {
-		const fields = [
-			{
-				fieldtype: 'Link',
-				fieldname: 'company',
-				label: __('Company'),
-				options: 'Company',
-				onchange: () => {
-					const value = this.field_group.get_value('company');
-
-					if (value) {
-						frappe.db.get_doc('Company', value)
-							.then(company => {
-								this.field_group.set_values({
-									country: company.country,
-									company_email: company.email,
-									currency: company.default_currency
-								});
-							});
-					}
-				}
-			},
-			{
-				fieldname: 'company_email',
-				label: __('Email'),
-				fieldtype: 'Data'
-			},
-			{
-				fieldname: 'country',
-				label: __('Country'),
-				fieldtype: 'Read Only'
-			},
-			{
-				fieldname: 'currency',
-				label: __('Currency'),
-				fieldtype: 'Read Only'
-			},
-			{
-				fieldtype: 'Text',
-				label: __('About your Company'),
-				fieldname: 'company_description'
-			}
-		];
-
-		this.field_group = new frappe.ui.FieldGroup({
-			parent: this.$form_container,
-			fields
-		});
-
-		this.field_group.make();
-
-		const default_company = frappe.defaults.get_default('company');
-		this.field_group.set_value('company', default_company);
-
-		this.$form_container.find('.form-column').append(`
-			<div class="text-right">
-				<button type="submit" class="btn btn-primary btn-register btn-sm">${__('Submit')}</button>
-			</div>
-		`);
-
-		this.$form_container.find('.form-message').removeClass('hidden small').addClass('h4').text(__('Become a Seller'))
-
-		this.$form_container.on('click', '.btn-register', (e) => {
-			const form_values = this.field_group.get_values();
-
-			let values_filled = true;
-			const mandatory_fields = ['company', 'company_email', 'company_description'];
-			mandatory_fields.forEach(field => {
-				const value = form_values[field];
-				if (!value) {
-					this.field_group.set_df_property(field, 'reqd', 1);
-					values_filled = false;
-				}
-			});
-			if (!values_filled) return;
-
-			frappe.call({
-				method: 'erpnext.hub_node.doctype.hub_settings.hub_settings.register_seller',
-				args: form_values,
-				btn: $(e.currentTarget)
-			}).then(() => {
-				frappe.set_route('marketplace', 'publish');
-
-				// custom jquery event
-				this.$wrapper.trigger('seller-registered');
-			});
-		});
-	}
-}
-
-erpnext.hub.Profile = class Profile extends SubPage {
-	make_wrapper() {
-		super.make_wrapper();
-	}
-
-	refresh() {
-		this.get_hub_seller_profile(this.keyword)
-			.then(profile => this.render(profile));
-	}
-
-	get_hub_seller_profile() {
-		return hub.call('get_hub_seller_profile', { hub_seller: hub.settings.company_email });
-	}
-
-	render(profile) {
-		const p = profile;
-		const content_by_log_type = this.get_content_by_log_type();
-
-		let activity_logs = (p.hub_seller_activity || []).sort((a, b) => {
-			return new Date(b.creation) - new Date(a.creation);
-		});
-
-		const timeline_items_html = activity_logs
-			.map(log => {
-				const stats = JSON.parse(log.stats);
-				const no_of_items = stats && stats.push_update || '';
-
-				const content = content_by_log_type[log.type];
-				const message = content.get_message(no_of_items);
-				const icon = content.icon;
-				return this.get_timeline_log_item(log.pretty_date, message, icon);
-			})
-			.join('');
-
-		const profile_html = `<div class="hub-item-container">
-			<div class="row visible-xs">
-				<div class="col-xs-12 margin-bottom">
-					<button class="btn btn-xs btn-default" data-route="marketplace/home">Back to home</button>
-				</div>
-			</div>
-			<div class="row">
-				<div class="col-md-3">
-					<div class="hub-item-image">
-						<img src="${p.logo}">
-					</div>
-				</div>
-				<div class="col-md-6">
-					<h2>${p.company}</h2>
-					<div class="text-muted">
-						<p>${p.country}</p>
-						<p>${p.site_name}</p>
-					</div>
-					<hr>
-					<div class="hub-item-description">
-					${'description'
-						? `<p>${p.company_description}</p>`
-						: `<p>__('No description')</p`
-					}
-					</div>
-				</div>
-			</div>
-
-			<div class="timeline">
-				<div class="timeline-items">
-					${timeline_items_html}
-				</div>
-			</div>
-
-		</div>`;
-
-		this.$wrapper.html(profile_html);
-	}
-
-	get_timeline_log_item(pretty_date, message, icon) {
-		return `<div class="media timeline-item  notification-content">
-			<div class="small">
-				<i class="octicon ${icon} fa-fw"></i>
-				<span title="Administrator"><b>${pretty_date}</b> ${message}</span>
-			</div>
-		</div>`;
-	}
-
-	get_content_by_log_type() {
-		return {
-			"Created": {
-				icon: 'octicon-heart',
-				get_message: () => 'Joined Marketplace'
-			},
-			"Items Publish": {
-				icon: 'octicon-bookmark',
-				get_message: (no_of_items) =>
-					`Published ${no_of_items} product${no_of_items > 1 ? 's' : ''} to Marketplace`
-			}
-		}
-	}
-}
-
-erpnext.hub.Publish = class Publish extends SubPage {
-	make_wrapper() {
-		super.make_wrapper();
-		this.items_to_publish = [];
-		this.unpublished_items = [];
-		this.fetched_items = [];
-
-		frappe.realtime.on("items-sync", (data) => {
-			this.$wrapper.find('.progress-bar').css('width', data.progress_percent+'%');
-
-			if(data.progress_percent === 100 || data.progress_percent === '100') {
-				setTimeout(() => {
-					hub.settings.sync_in_progress = 0;
-					frappe.db.get_doc('Hub Settings')
-						.then(doc => {
-							hub.settings = doc;
-							this.refresh();
-						});
-				}, 500);
-			}
-		});
-	}
-
-	refresh() {
-		if(!hub.settings.sync_in_progress) {
-			this.make_publish_ready_state();
-		} else {
-			this.make_publish_in_progress_state();
-		}
-	}
-
-	make_publish_ready_state() {
-		this.$wrapper.empty();
-		this.$wrapper.append(this.get_publishing_header());
-
-		make_search_bar({
-			wrapper: this.$wrapper,
-			on_search: keyword => {
-				this.search_value = keyword;
-				this.get_items_and_render();
-			},
-			placeholder: __('Search Items')
-		});
-
-		this.setup_publishing_events();
-
-		if(hub.settings.last_sync_datetime) {
-			this.show_message(`Last sync was <a href="#marketplace/profile">${comment_when(hub.settings.last_sync_datetime)}</a>.
-				<a href="#marketplace/my-products">See your Published Products</a>.`);
-		}
-
-		this.get_items_and_render();
-	}
-
-	get_publishing_header() {
-		const title_html = `<b>${__('Select Products to Publish')}</b>`;
-
-		const subtitle_html = `<p class="text-muted">
-			${__(`Only products with an image, description and category can be published.
-			Please update them if an item in your inventory does not appear.`)}
-		</p>`;
-
-		const publish_button_html = `<button class="btn btn-primary btn-sm publish-items">
-			<i class="visible-xs octicon octicon-check"></i>
-			<span class="hidden-xs">${__('Publish')}</span>
-		</button>`;
-
-		return $(`
-			<div class='subpage-title flex'>
-				<div>
-					${title_html}
-					${subtitle_html}
-				</div>
-				${publish_button_html}
-			</div>
-		`);
-	}
-
-	setup_publishing_events() {
-		this.$wrapper.find('.publish-items').on('click', () => {
-			this.publish_selected_items()
-				.then(this.refresh.bind(this))
-		});
-
-		this.$wrapper.on('click', '.hub-card', (e) => {
-			const $target = $(e.currentTarget);
-			$target.toggleClass('active');
-
-			// Get total items
-			const total_items = this.$wrapper.find('.hub-card.active').length;
-
-			let button_label;
-			if (total_items > 0) {
-				const more_than_one = total_items > 1;
-				button_label = __('Publish {0} item{1}', [total_items, more_than_one ? 's' : '']);
-			} else {
-				button_label = __('Publish');
-			}
-
-			this.$wrapper.find('.publish-items')
-				.text(button_label)
-				.prop('disabled', total_items === 0);
-		});
-	}
-
-	show_message(message) {
-		const $message = $(`<div class="subpage-message">
-			<p class="text-muted flex">
-				<span>
-					${message}
-				</span>
-				<i class="octicon octicon-x text-extra-muted"></i>
-			</p>
-		</div>`);
-
-		$message.find('.octicon-x').on('click', () => {
-			$message.remove();
-		});
-
-		this.$wrapper.prepend($message);
-	}
-
-	make_publish_in_progress_state() {
-		this.$wrapper.empty();
-
-		this.$wrapper.append(this.show_publish_progress());
-
-		const subtitle_html = `<p class="text-muted">
-			${__(`Only products with an image, description and category can be published.
-			Please update them if an item in your inventory does not appear.`)}
-		</p>`;
-
-		this.$wrapper.append(subtitle_html);
-
-		// Show search list with only desctiption, and don't set any events
-		make_search_bar({
-			wrapper: this.$wrapper,
-			on_search: keyword => {
-				this.search_value = keyword;
-				this.get_items_and_render();
-			},
-			placeholder: __('Search Items')
-		});
-
-		this.get_items_and_render();
-	}
-
-	show_publish_progress() {
-		const items_to_publish = this.items_to_publish.length
-			? this.items_to_publish
-			: JSON.parse(hub.settings.custom_data);
-
-		const $publish_progress = $(`<div class="sync-progress">
-			<p><b>${__(`Syncing ${items_to_publish.length} Products`)}</b></p>
-			<div class="progress">
-				<div class="progress-bar" style="width: 1%"></div>
-			</div>
-
-		</div>`);
-
-		const items_to_publish_container = $(get_item_card_container_html(
-			items_to_publish, '', get_local_item_card_html));
-
-		items_to_publish_container.find('.hub-card').addClass('active');
-
-		$publish_progress.append(items_to_publish_container);
-
-		return $publish_progress;
-	}
-
-	get_items_and_render(wrapper = this.$wrapper) {
-		wrapper.find('.results').remove();
-		const items = this.get_valid_items();
-
-		if(!items.then) {
-			this.render(items, wrapper);
-		} else {
-			items.then(r => {
-				this.fetched_items = r.message;
-				this.render(r.message, wrapper);
-			});
-		}
-	}
-
-	render(items, wrapper) {
-		const items_container = $(get_item_card_container_html(items, '', get_local_item_card_html));
-		items_container.addClass('results');
-		wrapper.append(items_container);
-	}
-
-	get_valid_items() {
-		if(this.unpublished_items.length) {
-			return this.unpublished_items;
-		}
-		return frappe.call(
-			'erpnext.hub_node.get_valid_items',
-			{
-				search_value: this.search_value
-			}
-		);
-	}
-
-	publish_selected_items() {
-		const item_codes_to_publish = [];
-		this.$wrapper.find('.hub-card.active').map(function () {
-			item_codes_to_publish.push($(this).attr("data-id"));
-		});
-
-		this.unpublished_items = this.fetched_items.filter(item => {
-			return !item_codes_to_publish.includes(item.item_code);
-		});
-
-		const items_to_publish = this.fetched_items.filter(item => {
-			return item_codes_to_publish.includes(item.item_code);
-		});
-		this.items_to_publish = items_to_publish;
-
-		return frappe.call(
-			'erpnext.hub_node.publish_selected_items',
-			{
-				items_to_publish: item_codes_to_publish
-			}
-		)
-	}
-}
-
-erpnext.hub.PublishedProducts = class PublishedProducts extends SubPage {
-	get_items_and_render() {
-		this.$wrapper.find('.hub-card-container').empty();
-		this.get_published_products()
-			.then(items => this.render(items));
-	}
-
-	refresh() {
-		this.get_items_and_render();
-	}
-
-	render(items) {
-		const items_container = $(get_item_card_container_html(items, __('Your Published Products')));
-		this.$wrapper.append(items_container);
-	}
-
-	get_published_products() {
-		return hub.call('get_items_by_seller', { hub_seller: hub.settings.company_email });
-	}
-}
-
-erpnext.hub.NotFound = class NotFound extends SubPage {
-	refresh() {
-		this.$wrapper.html(get_empty_state(
-			__('Sorry! I could not find what you were looking for.'),
-			`<button class="btn btn-default btn-xs" data-route="marketplace/home">${__('Back to home')}</button>`
-		));
-	}
-}
-
-function get_empty_state(message, action) {
-	return `<div class="empty-state flex align-center flex-column justify-center">
-		<p class="text-muted">${message}</p>
-		${action ? `<p>${action}</p>`: ''}
-	</div>`;
-}
-
-function get_item_card_container_html(items, title='', get_item_html = get_item_card_html) {
-	const items_html = (items || []).map(item => get_item_html(item)).join('');
-	const title_html = title
-		? `<div class="col-sm-12 margin-bottom">
-				<b>${title}</b>
-			</div>`
-		: '';
-
-	const html = `<div class="row hub-card-container">
-		${title_html}
-		${items_html}
-	</div>`;
-
-	return html;
-}
-
-function get_item_card_html(item) {
-	const item_name = item.item_name || item.name;
-	const title = strip_html(item_name);
-	const img_url = item.image;
-	const company_name = item.company;
-
-	// Subtitle
-	let subtitle = [comment_when(item.creation)];
-	const rating = item.average_rating;
-	if (rating > 0) {
-		subtitle.push(rating + `<i class='fa fa-fw fa-star-o'></i>`)
-	}
-	subtitle.push(company_name);
-
-	let dot_spacer = '<span aria-hidden="true"> · </span>';
-	subtitle = subtitle.join(dot_spacer);
-
-	const item_html = `
-		<div class="col-md-3 col-sm-4 col-xs-6">
-			<div class="hub-card" data-route="marketplace/item/${item.hub_item_code}">
-				<div class="hub-card-header">
-					<div class="hub-card-title ellipsis bold">${title}</div>
-					<div class="hub-card-subtitle ellipsis text-muted">${subtitle}</div>
-				</div>
-				<div class="hub-card-body">
-					<img class="hub-card-image" src="${img_url}" />
-					<div class="overlay hub-card-overlay"></div>
-				</div>
-			</div>
-		</div>
-	`;
-
-	return item_html;
-}
-
-function get_local_item_card_html(item) {
-	const item_name = item.item_name || item.name;
-	const title = strip_html(item_name);
-	const img_url = item.image;
-	const company_name = item.company;
-
-	const is_active = item.publish_in_hub;
-	const id = item.hub_item_code || item.item_code;
-
-	// Subtitle
-	let subtitle = [comment_when(item.creation)];
-	const rating = item.average_rating;
-	if (rating > 0) {
-		subtitle.push(rating + `<i class='fa fa-fw fa-star-o'></i>`)
-	}
-	subtitle.push(company_name);
-
-	let dot_spacer = '<span aria-hidden="true"> · </span>';
-	subtitle = subtitle.join(dot_spacer);
-
-	const edit_item_button = `<div class="hub-card-overlay-button" style="right: 15px; bottom: 15px;" data-route="Form/Item/${item.item_name}">
-		<button class="btn btn-default zoom-view">
-			<i class="octicon octicon-pencil text-muted"></i>
-		</button>
-	</div>`;
-
-	const item_html = `
-		<div class="col-md-3 col-sm-4 col-xs-6">
-			<div class="hub-card is-local ${is_active ? 'active' : ''}" data-id="${id}">
-				<div class="hub-card-header">
-					<div class="hub-card-title ellipsis bold">${title}</div>
-					<div class="hub-card-subtitle ellipsis text-muted">${subtitle}</div>
-					<i class="octicon octicon-check text-success"></i>
-				</div>
-				<div class="hub-card-body">
-					<img class="hub-card-image" src="${img_url}" />
-					<div class="hub-card-overlay">
-						<div class="hub-card-overlay-body">
-							${edit_item_button}
-						</div>
-					</div>
-				</div>
-			</div>
-		</div>
-	`;
-
-	return item_html;
-}
-
-
-function get_rating_html(rating) {
-	let rating_html = ``;
-	for (var i = 0; i < 5; i++) {
-		let star_class = 'fa-star';
-		if (i >= rating) star_class = 'fa-star-o';
-		rating_html += `<i class='fa fa-fw ${star_class} star-icon' data-index=${i}></i>`;
-	}
-	return rating_html;
-}
-
-function make_search_bar({wrapper, on_search, placeholder = __('Search for anything')}) {
-	const $search = $(`
-		<div class="hub-search-container">
-			<input type="text" class="form-control" placeholder="${placeholder}">
-		</div>`
-	);
-	wrapper.append($search);
-	const $search_input = $search.find('input');
-
-	$search_input.on('keydown', frappe.utils.debounce((e) => {
-		if (e.which === frappe.ui.keyCode.ENTER) {
-			const search_value = $search_input.val();
-			on_search(search_value);
-		}
-	}, 300));
-}
-
-// caching
-
-erpnext.hub.cache = {};
-hub.call = function call_hub_method(method, args={}) {
-	return new Promise((resolve, reject) => {
-
-		// cache
-		const key = method + JSON.stringify(args);
-		if (erpnext.hub.cache[key]) {
-			resolve(erpnext.hub.cache[key]);
-		}
-
-		// cache invalidation after 5 minutes
-		const timeout = 5 * 60 * 1000;
-
-		setTimeout(() => {
-			delete erpnext.hub.cache[key];
-		}, timeout);
-
-		frappe.call({
-			method: 'erpnext.hub_node.call_hub_method',
-			args: {
-				method,
-				params: args
-			}
-		})
-		.then(r => {
-			if (r.message) {
-				if (r.message.error) {
-					frappe.throw({
-						title: __('Marketplace Error'),
-						message: r.message.error
-					});
-				}
-
-				erpnext.hub.cache[key] = r.message;
-				resolve(r.message)
-			}
-			reject(r)
-		})
-		.fail(reject)
-	});
-}
diff --git a/erpnext/public/js/hub/pages/base_page.js b/erpnext/public/js/hub/pages/base_page.js
new file mode 100644
index 0000000..70248da
--- /dev/null
+++ b/erpnext/public/js/hub/pages/base_page.js
@@ -0,0 +1,35 @@
+export default class SubPage {
+	constructor(parent, options) {
+		this.$parent = $(parent);
+		this.make_wrapper(options);
+
+		// handle broken images after every render
+		if (this.render) {
+			this._render = this.render.bind(this);
+
+			this.render = (...args) => {
+				this._render(...args);
+				frappe.dom.handle_broken_images(this.$wrapper);
+			}
+		}
+	}
+
+	make_wrapper() {
+		const page_name = frappe.get_route()[1];
+		this.$wrapper = $(`<div class="marketplace-page" data-page-name="${page_name}">`).appendTo(this.$parent);
+		this.hide();
+	}
+
+	empty() {
+		this.$wrapper.empty();
+	}
+
+	show() {
+		this.refresh();
+		this.$wrapper.show();
+	}
+
+	hide() {
+		this.$wrapper.hide();
+	}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/category.js b/erpnext/public/js/hub/pages/category.js
new file mode 100644
index 0000000..21dcb32
--- /dev/null
+++ b/erpnext/public/js/hub/pages/category.js
@@ -0,0 +1,27 @@
+import SubPage from './base_page';
+import { get_item_card_container_html } from '../helpers';
+
+erpnext.hub.Category = class Category extends SubPage {
+	refresh() {
+		this.category = frappe.get_route()[2];
+		this.get_items_for_category(this.category)
+			.then(r => {
+				this.render(r.message);
+			});
+	}
+
+	get_items_for_category(category) {
+		this.$wrapper.find('.hub-card-container').empty();
+		return frappe.call('erpnext.hub_node.get_list', {
+			doctype: 'Hub Item',
+			filters: {
+				hub_category: category
+			}
+		});
+	}
+
+	render(items) {
+		const html = get_item_card_container_html(items, __(this.category));
+		this.$wrapper.append(html)
+	}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/favourites.js b/erpnext/public/js/hub/pages/favourites.js
new file mode 100644
index 0000000..9605eb1
--- /dev/null
+++ b/erpnext/public/js/hub/pages/favourites.js
@@ -0,0 +1,21 @@
+import SubPage from './base_page';
+import { get_item_card_container_html } from '../helpers';
+
+erpnext.hub.Favourites = class Favourites extends SubPage {
+	refresh() {
+		this.get_favourites()
+			.then(items => {
+				this.render(items);
+			});
+	}
+
+	get_favourites() {
+		return hub.call('get_item_favourites');
+	}
+
+	render(items) {
+		this.$wrapper.find('.hub-card-container').empty();
+		const html = get_item_card_container_html(items, __('Favourites'));
+		this.$wrapper.append(html)
+	}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/home.js b/erpnext/public/js/hub/pages/home.js
new file mode 100644
index 0000000..ff37e81
--- /dev/null
+++ b/erpnext/public/js/hub/pages/home.js
@@ -0,0 +1,41 @@
+import SubPage from './base_page';
+import { make_search_bar, get_item_card_container_html } from '../helpers';
+
+erpnext.hub.Home = class Home extends SubPage {
+	make_wrapper() {
+		super.make_wrapper();
+
+		make_search_bar({
+			wrapper: this.$wrapper,
+			on_search: keyword => {
+				frappe.set_route('marketplace', 'search', keyword);
+			}
+		});
+	}
+
+	refresh() {
+		this.get_items_and_render();
+	}
+
+	get_items_and_render() {
+		this.$wrapper.find('.hub-card-container').empty();
+		this.get_data()
+			.then(data => {
+				this.render(data);
+			});
+	}
+
+	get_data() {
+		return hub.call('get_data_for_homepage', { country: frappe.defaults.get_user_default('country') });
+	}
+
+	render(data) {
+		let html = get_item_card_container_html(data.random_items, __('Explore'));
+		this.$wrapper.append(html);
+
+		if (data.items_by_country.length) {
+			html = get_item_card_container_html(data.items_by_country, __('Near you'));
+			this.$wrapper.append(html);
+		}
+	}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/item.js b/erpnext/public/js/hub/pages/item.js
new file mode 100644
index 0000000..dabddea
--- /dev/null
+++ b/erpnext/public/js/hub/pages/item.js
@@ -0,0 +1,327 @@
+import SubPage from './base_page';
+import { get_rating_html } from '../helpers';
+
+erpnext.hub.Item = class Item extends SubPage {
+	make_wrapper() {
+		super.make_wrapper();
+		this.setup_events();
+	}
+
+	refresh() {
+		this.show_skeleton();
+		this.hub_item_code = frappe.get_route()[2];
+
+		this.own_item = false;
+
+		this.get_item(this.hub_item_code)
+			.then(item => {
+				this.own_item = item.hub_seller === hub.settings.company_email;
+				this.item = item;
+				this.render(item);
+			});
+	}
+
+	show_skeleton() {
+		const skeleton = `<div class="hub-item-container">
+			<div class="row">
+				<div class="col-md-3">
+					<div class="hub-item-skeleton-image"></div>
+				</div>
+				<div class="col-md-6">
+					<h2 class="hub-skeleton" style="width: 75%;">Name</h2>
+					<div class="text-muted">
+						<p class="hub-skeleton" style="width: 35%;">Details</p>
+						<p class="hub-skeleton" style="width: 50%;">Ratings</p>
+					</div>
+					<hr>
+					<div class="hub-item-description">
+						<p class="hub-skeleton">Desc</p>
+						<p class="hub-skeleton" style="width: 85%;">Desc</p>
+					</div>
+				</div>
+			</div>
+		</div>`;
+
+		this.$wrapper.html(skeleton);
+	}
+
+	setup_events() {
+		this.$wrapper.on('click', '.btn-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')
+					}
+				]
+			});
+
+			d.show();
+		});
+	}
+
+	get_item(hub_item_code) {
+		return hub.call('get_item_details', { hub_item_code });
+	}
+
+	render(item) {
+		const title = item.item_name || item.name;
+		const seller = item.company;
+
+		const who = __('Posted By {0}', [seller]);
+		const when = comment_when(item.creation);
+
+		const city = item.city ? item.city + ', ' : '';
+		const country = item.country ? item.country : '';
+		const where = `${city}${country}`;
+
+		const dot_spacer = '<span aria-hidden="true"> · </span>';
+
+		const description = item.description || '';
+
+		const rating_html = get_rating_html(item.average_rating);
+		const rating_count = item.no_of_ratings > 0 ? `${item.no_of_ratings} reviews` : __('No reviews yet');
+
+		let edit_buttons_html = '';
+
+		if(this.own_item) {
+			edit_buttons_html = `<div style="margin-top: 20px">
+				<button class="btn btn-secondary btn-default btn-xs margin-right edit-item">Edit Details</button>
+				<button class="btn btn-secondary btn-danger btn-xs unpublish">Unpublish</button>
+			</div>`;
+		}
+
+		const html = `
+			<div class="hub-item-container">
+				<div class="row visible-xs">
+					<div class="col-xs-12 margin-bottom">
+						<button class="btn btn-xs btn-default" data-route="marketplace/home">Back to home</button>
+					</div>
+				</div>
+				<div class="row">
+					<div class="col-md-3">
+						<div class="hub-item-image">
+							<img src="${item.image}">
+						</div>
+					</div>
+					<div class="col-md-8">
+						<h2>${title}</h2>
+						<div class="text-muted">
+							<p>${where}${dot_spacer}${when}</p>
+							<p>${rating_html} (${rating_count})</p>
+						</div>
+						<hr>
+						<div class="hub-item-description">
+						${description ?
+							`<b>${__('Description')}</b>
+							<p>${description}</p>
+							` : `<p>${__('No description')}<p>`
+						}
+						</div>
+						${edit_buttons_html}
+					</div>
+					<div 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><a>Edit Details</a></li>
+								<li><a>Unpublish</a></li>
+							</ul>
+						</div>
+					</div>
+				</div>
+				<div class="row hub-item-seller">
+					<div class="col-md-12 margin-top margin-bottom">
+						<b class="text-muted">Seller Information</b>
+					</div>
+					<div class="col-md-1">
+						<img src="https://picsum.photos/200">
+					</div>
+					<div class="col-md-8">
+						<div class="margin-bottom"><a href="#marketplace/seller/${seller}" class="bold">${seller}</a></div>
+						<button class="btn btn-xs btn-default text-muted btn-contact-seller">
+							${__('Contact Seller')}
+						</button>
+					</div>
+				</div>
+				<!-- review area -->
+				<div class="row hub-item-review-container">
+					<div class="col-md-12 form-footer">
+						<div class="form-comments">
+							<div class="timeline">
+								<div class="timeline-head"></div>
+								<div class="timeline-items"></div>
+							</div>
+						</div>
+						<div class="pull-right scroll-to-top">
+							<a onclick="frappe.utils.scroll_to(0)"><i class="fa fa-chevron-up text-muted"></i></a>
+						</div>
+					</div>
+				</div>
+			</div>
+		`;
+
+		this.$wrapper.html(html);
+
+		if(this.own_item) {
+			this.bind_edit_buttons();
+		}
+
+		this.make_review_area();
+
+		this.get_reviews()
+			.then(reviews => {
+				this.reviews = reviews;
+				this.render_reviews(reviews);
+			});
+	}
+
+	bind_edit_buttons() {
+		this.edit_dialog = new frappe.ui.Dialog({
+			title: "Edit Your Product",
+			fields: []
+		});
+
+		this.$wrapper.find('.edit-item').on('click', this.on_edit.bind(this));
+		this.$wrapper.find('.unpublish').on('click', this.on_unpublish.bind(this));
+	}
+
+	on_edit() {
+		this.edit_dialog.show();
+	}
+
+	on_unpublish() {
+		if(!this.unpublish_dialog) {
+			this.unpublish_dialog = new frappe.ui.Dialog({
+				title: "Edit Your Product",
+				fields: []
+			});
+		}
+
+		this.unpublish_dialog.show();
+	}
+
+	make_review_area() {
+		this.comment_area = new frappe.ui.ReviewArea({
+			parent: this.$wrapper.find('.timeline-head').empty(),
+			mentions: [],
+			on_submit: (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(review => {
+					this.reviews = this.reviews || [];
+					this.reviews.push(review);
+					this.render_reviews(this.reviews);
+
+					this.comment_area.reset();
+				});
+			}
+		});
+	}
+
+	get_reviews() {
+		return hub.call('get_item_reviews', { hub_item_code: this.hub_item_code }).catch(() => {});
+	}
+
+	render_reviews(reviews=[]) {
+		this.$wrapper.find('.timeline-items').empty();
+
+		reviews.sort((a, b) => {
+			if (a.modified > b.modified) {
+				return -1;
+			}
+
+			if (a.modified < b.modified) {
+				return 1;
+			}
+
+			return 0;
+		});
+
+		reviews.forEach(review => this.render_review(review));
+	}
+
+	render_review(review) {
+		let username = review.username || review.user || __("Anonymous");
+
+		let image_html = review.user_image
+			? `<div class="avatar-frame" style="background-image: url(${review.user_image})"></div>`
+			: `<div class="standard-image" style="background-color: #fafbfc">${frappe.get_abbr(username)}</div>`
+
+		let edit_html = review.own
+			? `<div class="pull-right hidden-xs close-btn-container">
+				<span class="small text-muted">
+					${'data.delete'}
+				</span>
+			</div>
+			<div class="pull-right edit-btn-container">
+				<span class="small text-muted">
+					${'data.edit'}
+				</span>
+			</div>`
+			: '';
+
+		let rating_html = get_rating_html(review.rating);
+
+		const $timeline_items = this.$wrapper.find('.timeline-items');
+
+		$(this.get_timeline_item(review, image_html, edit_html, rating_html))
+			.appendTo($timeline_items);
+	}
+
+	get_timeline_item(data, image_html, edit_html, rating_html) {
+		return `<div class="media timeline-item user-content" data-doctype="${''}" data-name="${''}">
+			<span class="pull-left avatar avatar-medium hidden-xs" style="margin-top: 1px">
+				${image_html}
+			</span>
+			<div class="pull-left media-body">
+				<div class="media-content-wrapper">
+					<div class="action-btns">${edit_html}</div>
+
+					<div class="comment-header clearfix">
+						<span class="pull-left avatar avatar-small visible-xs">
+							${image_html}
+						</span>
+
+						<div class="asset-details">
+							<span class="author-wrap">
+								<i class="octicon octicon-quote hidden-xs fa-fw"></i>
+								<span>${data.username}</span>
+							</span>
+							<a class="text-muted">
+								<span class="text-muted hidden-xs">&ndash;</span>
+								<span class="hidden-xs">${comment_when(data.modified)}</span>
+							</a>
+						</div>
+					</div>
+					<div class="reply timeline-content-show">
+						<div class="timeline-item-content">
+							<p class="text-muted">
+								${rating_html}
+							</p>
+							<h6 class="bold">${data.subject}</h6>
+							<p class="text-muted">
+								${data.content}
+							</p>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>`;
+	}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/not_found.js b/erpnext/public/js/hub/pages/not_found.js
new file mode 100644
index 0000000..a83d881
--- /dev/null
+++ b/erpnext/public/js/hub/pages/not_found.js
@@ -0,0 +1,10 @@
+import SubPage from './base_page';
+
+erpnext.hub.NotFound = class NotFound extends SubPage {
+	refresh() {
+		this.$wrapper.html(get_empty_state(
+			__('Sorry! I could not find what you were looking for.'),
+			`<button class="btn btn-default btn-xs" data-route="marketplace/home">${__('Back to home')}</button>`
+		));
+	}
+}
diff --git a/erpnext/public/js/hub/pages/profile.js b/erpnext/public/js/hub/pages/profile.js
new file mode 100644
index 0000000..6dd1f87
--- /dev/null
+++ b/erpnext/public/js/hub/pages/profile.js
@@ -0,0 +1,98 @@
+import SubPage from './base_page';
+
+erpnext.hub.Profile = class Profile extends SubPage {
+	make_wrapper() {
+		super.make_wrapper();
+	}
+
+	refresh() {
+		this.get_hub_seller_profile(this.keyword)
+			.then(profile => this.render(profile));
+	}
+
+	get_hub_seller_profile() {
+		return hub.call('get_hub_seller_profile', { hub_seller: hub.settings.company_email });
+	}
+
+	render(profile) {
+		const p = profile;
+		const content_by_log_type = this.get_content_by_log_type();
+
+		let activity_logs = (p.hub_seller_activity || []).sort((a, b) => {
+			return new Date(b.creation) - new Date(a.creation);
+		});
+
+		const timeline_items_html = activity_logs
+			.map(log => {
+				const stats = JSON.parse(log.stats);
+				const no_of_items = stats && stats.push_update || '';
+
+				const content = content_by_log_type[log.type];
+				const message = content.get_message(no_of_items);
+				const icon = content.icon;
+				return this.get_timeline_log_item(log.pretty_date, message, icon);
+			})
+			.join('');
+
+		const profile_html = `<div class="hub-item-container">
+			<div class="row visible-xs">
+				<div class="col-xs-12 margin-bottom">
+					<button class="btn btn-xs btn-default" data-route="marketplace/home">Back to home</button>
+				</div>
+			</div>
+			<div class="row">
+				<div class="col-md-3">
+					<div class="hub-item-image">
+						<img src="${p.logo}">
+					</div>
+				</div>
+				<div class="col-md-6">
+					<h2>${p.company}</h2>
+					<div class="text-muted">
+						<p>${p.country}</p>
+						<p>${p.site_name}</p>
+					</div>
+					<hr>
+					<div class="hub-item-description">
+					${'description'
+						? `<p>${p.company_description}</p>`
+						: `<p>__('No description')</p`
+					}
+					</div>
+				</div>
+			</div>
+
+			<div class="timeline">
+				<div class="timeline-items">
+					${timeline_items_html}
+				</div>
+			</div>
+
+		</div>`;
+
+		this.$wrapper.html(profile_html);
+	}
+
+	get_timeline_log_item(pretty_date, message, icon) {
+		return `<div class="media timeline-item  notification-content">
+			<div class="small">
+				<i class="octicon ${icon} fa-fw"></i>
+				<span title="Administrator"><b>${pretty_date}</b> ${message}</span>
+			</div>
+		</div>`;
+	}
+
+	get_content_by_log_type() {
+		return {
+			"Created": {
+				icon: 'octicon-heart',
+				get_message: () => 'Joined Marketplace'
+			},
+			"Items Publish": {
+				icon: 'octicon-bookmark',
+				get_message: (no_of_items) =>
+					`Published ${no_of_items} product${no_of_items > 1 ? 's' : ''} to Marketplace`
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/publish.js b/erpnext/public/js/hub/pages/publish.js
new file mode 100644
index 0000000..6e8caab
--- /dev/null
+++ b/erpnext/public/js/hub/pages/publish.js
@@ -0,0 +1,228 @@
+import SubPage from './base_page';
+import { make_search_bar, get_item_card_container_html, get_local_item_card_html } from '../helpers';
+
+erpnext.hub.Publish = class Publish extends SubPage {
+	make_wrapper() {
+		super.make_wrapper();
+		this.items_to_publish = [];
+		this.unpublished_items = [];
+		this.fetched_items = [];
+
+		frappe.realtime.on("items-sync", (data) => {
+			this.$wrapper.find('.progress-bar').css('width', data.progress_percent+'%');
+
+			if(data.progress_percent === 100 || data.progress_percent === '100') {
+				setTimeout(() => {
+					hub.settings.sync_in_progress = 0;
+					frappe.db.get_doc('Hub Settings')
+						.then(doc => {
+							hub.settings = doc;
+							this.refresh();
+						});
+				}, 500);
+			}
+		});
+	}
+
+	refresh() {
+		if(!hub.settings.sync_in_progress) {
+			this.make_publish_ready_state();
+		} else {
+			this.make_publish_in_progress_state();
+		}
+	}
+
+	make_publish_ready_state() {
+		this.$wrapper.empty();
+		this.$wrapper.append(this.get_publishing_header());
+
+		make_search_bar({
+			wrapper: this.$wrapper,
+			on_search: keyword => {
+				this.search_value = keyword;
+				this.get_items_and_render();
+			},
+			placeholder: __('Search Items')
+		});
+
+		this.setup_publishing_events();
+
+		if(hub.settings.last_sync_datetime) {
+			this.show_message(`Last sync was <a href="#marketplace/profile">${comment_when(hub.settings.last_sync_datetime)}</a>.
+				<a href="#marketplace/my-products">See your Published Products</a>.`);
+		}
+
+		this.get_items_and_render();
+	}
+
+	get_publishing_header() {
+		const title_html = `<b>${__('Select Products to Publish')}</b>`;
+
+		const subtitle_html = `<p class="text-muted">
+			${__(`Only products with an image, description and category can be published.
+			Please update them if an item in your inventory does not appear.`)}
+		</p>`;
+
+		const publish_button_html = `<button class="btn btn-primary btn-sm publish-items">
+			<i class="visible-xs octicon octicon-check"></i>
+			<span class="hidden-xs">${__('Publish')}</span>
+		</button>`;
+
+		return $(`
+			<div class='subpage-title flex'>
+				<div>
+					${title_html}
+					${subtitle_html}
+				</div>
+				${publish_button_html}
+			</div>
+		`);
+	}
+
+	setup_publishing_events() {
+		this.$wrapper.find('.publish-items').on('click', () => {
+			this.publish_selected_items()
+				.then(this.refresh.bind(this))
+		});
+
+		this.$wrapper.on('click', '.hub-card', (e) => {
+			const $target = $(e.currentTarget);
+			$target.toggleClass('active');
+
+			// Get total items
+			const total_items = this.$wrapper.find('.hub-card.active').length;
+
+			let button_label;
+			if (total_items > 0) {
+				const more_than_one = total_items > 1;
+				button_label = __('Publish {0} item{1}', [total_items, more_than_one ? 's' : '']);
+			} else {
+				button_label = __('Publish');
+			}
+
+			this.$wrapper.find('.publish-items')
+				.text(button_label)
+				.prop('disabled', total_items === 0);
+		});
+	}
+
+	show_message(message) {
+		const $message = $(`<div class="subpage-message">
+			<p class="text-muted flex">
+				<span>
+					${message}
+				</span>
+				<i class="octicon octicon-x text-extra-muted"></i>
+			</p>
+		</div>`);
+
+		$message.find('.octicon-x').on('click', () => {
+			$message.remove();
+		});
+
+		this.$wrapper.prepend($message);
+	}
+
+	make_publish_in_progress_state() {
+		this.$wrapper.empty();
+
+		this.$wrapper.append(this.show_publish_progress());
+
+		const subtitle_html = `<p class="text-muted">
+			${__(`Only products with an image, description and category can be published.
+			Please update them if an item in your inventory does not appear.`)}
+		</p>`;
+
+		this.$wrapper.append(subtitle_html);
+
+		// Show search list with only desctiption, and don't set any events
+		make_search_bar({
+			wrapper: this.$wrapper,
+			on_search: keyword => {
+				this.search_value = keyword;
+				this.get_items_and_render();
+			},
+			placeholder: __('Search Items')
+		});
+
+		this.get_items_and_render();
+	}
+
+	show_publish_progress() {
+		const items_to_publish = this.items_to_publish.length
+			? this.items_to_publish
+			: JSON.parse(hub.settings.custom_data);
+
+		const $publish_progress = $(`<div class="sync-progress">
+			<p><b>${__(`Syncing ${items_to_publish.length} Products`)}</b></p>
+			<div class="progress">
+				<div class="progress-bar" style="width: 1%"></div>
+			</div>
+
+		</div>`);
+
+		const items_to_publish_container = $(get_item_card_container_html(
+			items_to_publish, '', get_local_item_card_html));
+
+		items_to_publish_container.find('.hub-card').addClass('active');
+
+		$publish_progress.append(items_to_publish_container);
+
+		return $publish_progress;
+	}
+
+	get_items_and_render(wrapper = this.$wrapper) {
+		wrapper.find('.results').remove();
+		const items = this.get_valid_items();
+
+		if(!items.then) {
+			this.render(items, wrapper);
+		} else {
+			items.then(r => {
+				this.fetched_items = r.message;
+				this.render(r.message, wrapper);
+			});
+		}
+	}
+
+	render(items, wrapper) {
+		const items_container = $(get_item_card_container_html(items, '', get_local_item_card_html));
+		items_container.addClass('results');
+		wrapper.append(items_container);
+	}
+
+	get_valid_items() {
+		if(this.unpublished_items.length) {
+			return this.unpublished_items;
+		}
+		return frappe.call(
+			'erpnext.hub_node.get_valid_items',
+			{
+				search_value: this.search_value
+			}
+		);
+	}
+
+	publish_selected_items() {
+		const item_codes_to_publish = [];
+		this.$wrapper.find('.hub-card.active').map(function () {
+			item_codes_to_publish.push($(this).attr("data-id"));
+		});
+
+		this.unpublished_items = this.fetched_items.filter(item => {
+			return !item_codes_to_publish.includes(item.item_code);
+		});
+
+		const items_to_publish = this.fetched_items.filter(item => {
+			return item_codes_to_publish.includes(item.item_code);
+		});
+		this.items_to_publish = items_to_publish;
+
+		return frappe.call(
+			'erpnext.hub_node.publish_selected_items',
+			{
+				items_to_publish: item_codes_to_publish
+			}
+		)
+	}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/published_products.js b/erpnext/public/js/hub/pages/published_products.js
new file mode 100644
index 0000000..f21c6fa
--- /dev/null
+++ b/erpnext/public/js/hub/pages/published_products.js
@@ -0,0 +1,23 @@
+import SubPage from './base_page';
+import { get_item_card_container_html } from '../helpers';
+
+erpnext.hub.PublishedProducts = class PublishedProducts extends SubPage {
+	get_items_and_render() {
+		this.$wrapper.find('.hub-card-container').empty();
+		this.get_published_products()
+			.then(items => this.render(items));
+	}
+
+	refresh() {
+		this.get_items_and_render();
+	}
+
+	render(items) {
+		const items_container = $(get_item_card_container_html(items, __('Your Published Products')));
+		this.$wrapper.append(items_container);
+	}
+
+	get_published_products() {
+		return hub.call('get_items_by_seller', { hub_seller: hub.settings.company_email });
+	}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/hub/pages/register.js b/erpnext/public/js/hub/pages/register.js
new file mode 100644
index 0000000..d8966f1
--- /dev/null
+++ b/erpnext/public/js/hub/pages/register.js
@@ -0,0 +1,110 @@
+import SubPage from './base_page';
+
+erpnext.hub.Register = class Register extends SubPage {
+	make_wrapper() {
+		super.make_wrapper();
+		this.$register_container = $(`<div class="row register-container">`)
+			.appendTo(this.$wrapper);
+		this.$form_container = $('<div class="col-md-8 col-md-offset-1 form-container">')
+			.appendTo(this.$wrapper);
+	}
+
+	refresh() {
+		this.$register_container.empty();
+		this.$form_container.empty();
+		this.render();
+	}
+
+	render() {
+		this.make_field_group();
+	}
+
+	make_field_group() {
+		const fields = [
+			{
+				fieldtype: 'Link',
+				fieldname: 'company',
+				label: __('Company'),
+				options: 'Company',
+				onchange: () => {
+					const value = this.field_group.get_value('company');
+
+					if (value) {
+						frappe.db.get_doc('Company', value)
+							.then(company => {
+								this.field_group.set_values({
+									country: company.country,
+									company_email: company.email,
+									currency: company.default_currency
+								});
+							});
+					}
+				}
+			},
+			{
+				fieldname: 'company_email',
+				label: __('Email'),
+				fieldtype: 'Data'
+			},
+			{
+				fieldname: 'country',
+				label: __('Country'),
+				fieldtype: 'Read Only'
+			},
+			{
+				fieldname: 'currency',
+				label: __('Currency'),
+				fieldtype: 'Read Only'
+			},
+			{
+				fieldtype: 'Text',
+				label: __('About your Company'),
+				fieldname: 'company_description'
+			}
+		];
+
+		this.field_group = new frappe.ui.FieldGroup({
+			parent: this.$form_container,
+			fields
+		});
+
+		this.field_group.make();
+
+		const default_company = frappe.defaults.get_default('company');
+		this.field_group.set_value('company', default_company);
+
+		this.$form_container.find('.form-column').append(`
+			<div class="text-right">
+				<button type="submit" class="btn btn-primary btn-register btn-sm">${__('Submit')}</button>
+			</div>
+		`);
+
+		this.$form_container.find('.form-message').removeClass('hidden small').addClass('h4').text(__('Become a Seller'))
+
+		this.$form_container.on('click', '.btn-register', (e) => {
+			const form_values = this.field_group.get_values();
+
+			let values_filled = true;
+			const mandatory_fields = ['company', 'company_email', 'company_description'];
+			mandatory_fields.forEach(field => {
+				const value = form_values[field];
+				if (!value) {
+					this.field_group.set_df_property(field, 'reqd', 1);
+					values_filled = false;
+				}
+			});
+			if (!values_filled) return;
+
+			frappe.call({
+				method: 'erpnext.hub_node.doctype.hub_settings.hub_settings.register_seller',
+				args: form_values,
+				btn: $(e.currentTarget)
+			}).then(() => {
+				frappe.set_route('marketplace', 'publish');
+
+				// custom jquery event
+				this.$wrapper.trigger('seller-registered');
+			});
+		});
+	}
+}
diff --git a/erpnext/public/js/hub/pages/search.js b/erpnext/public/js/hub/pages/search.js
new file mode 100644
index 0000000..276c9bc
--- /dev/null
+++ b/erpnext/public/js/hub/pages/search.js
@@ -0,0 +1,34 @@
+import SubPage from './base_page';
+import { make_search_bar, get_item_card_container_html } from '../helpers';
+
+erpnext.hub.SearchPage = class SearchPage extends SubPage {
+	make_wrapper() {
+		super.make_wrapper();
+
+		make_search_bar({
+			wrapper: this.$wrapper,
+			on_search: keyword => {
+				frappe.set_route('marketplace', 'search', keyword);
+			}
+		});
+	}
+
+	refresh() {
+		this.keyword = frappe.get_route()[2] || '';
+		this.$wrapper.find('input').val(this.keyword);
+
+		this.get_items_by_keyword(this.keyword)
+			.then(items => this.render(items));
+	}
+
+	get_items_by_keyword(keyword) {
+		return hub.call('get_items_by_keyword', { keyword });
+	}
+
+	render(items) {
+		this.$wrapper.find('.hub-card-container').remove();
+		const title = this.keyword ? __('Search results for "{0}"', [this.keyword]) : '';
+		const html = get_item_card_container_html(items, title);
+		this.$wrapper.append(html);
+	}
+}
\ No newline at end of file