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