New page: marketplace

- subpages: home, favourites, item, category
This commit is contained in:
Faris Ansari 2018-07-23 18:44:46 +05:30
parent f585197045
commit ea2d1b0a19
4 changed files with 503 additions and 42 deletions

View File

@ -21,7 +21,7 @@ def get_list(doctype, start=0, limit=20, fields=["*"], filters="{}", order_by=No
response = connection.get_list(doctype, response = connection.get_list(doctype,
limit_start=start, limit_page_length=limit, limit_start=start, limit_page_length=limit,
filters=filters, fields=fields) filters=filters, fields=['name'])
# Bad, need child tables in response # Bad, need child tables in response
listing = [] listing = []

View File

@ -1,6 +1,32 @@
frappe.provide('erpnext.hub.pages'); frappe.provide('erpnext.hub.pages');
frappe.views.HubFactory = frappe.views.Factory.extend({ 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();
} else {
this.make('marketplace');
}
}
make(page_name) {
const assets = [
'/assets/erpnext/js/hub/hub_listing.js'
];
frappe.require(assets, () => {
erpnext.hub.marketplace = new erpnext.hub.Marketplace({
parent: this.make_page(true, page_name)
});
});
}
}
frappe.views.HubFactory = class HubFactory extends frappe.views.Factory {
make(route) { make(route) {
const page_name = frappe.get_route_str(); const page_name = frappe.get_route_str();
const page = route[1]; const page = route[1];
@ -60,7 +86,7 @@ frappe.views.HubFactory = frappe.views.Factory.extend({
window.hub_page = erpnext.hub.pages[page_name]; window.hub_page = erpnext.hub.pages[page_name];
} }
}); });
}, }
render_offline_card() { render_offline_card() {
let html = `<div class='page-card' style='margin: 140px auto;'> let html = `<div class='page-card' style='margin: 140px auto;'>
@ -77,4 +103,4 @@ frappe.views.HubFactory = frappe.views.Factory.extend({
return; return;
} }
}); }

View File

@ -1,5 +1,404 @@
frappe.provide('erpnext.hub'); frappe.provide('erpnext.hub');
erpnext.hub.Marketplace = class Marketplace {
constructor({ parent }) {
this.$parent = $(parent);
this.page = parent.page;
this.setup_header();
this.make_sidebar();
this.make_body();
this.setup_events();
this.refresh();
}
setup_header() {
this.page.set_title(__('Marketplace'));
}
setup_events() {
this.$parent.on('click', '[data-route]', (e) => {
const $target = $(e.currentTarget);
const route = $target.data().route;
frappe.set_route(route);
});
}
make_sidebar() {
this.$sidebar = this.$parent.find('.layout-side-section');
this.$sidebar.append(`
<ul class="list-unstyled hub-sidebar-group">
<li class="hub-sidebar-item" data-route="marketplace/home">
${__('Browse')}
</li>
<li class="hub-sidebar-item" data-route="marketplace/favourites">
${__('Favorites')}
</li>
<li class="hub-sidebar-item text-muted">
${__('Become a seller')}
</li>
</ul>
`);
this.make_sidebar_categories();
}
make_sidebar_categories() {
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 is-title">
${__('Category')}
</li>`,
`<li class="hub-sidebar-item active" data-route="marketplace/home">
${__('All')}
</li>`,
...categories.map(category => `
<li class="hub-sidebar-item text-muted" data-route="marketplace/category/${category}">
${__(category)}
</li>
`)
];
this.$sidebar.append(`
<ul class="list-unstyled">
${sidebar_items.join('')}
</ul>
`);
this.update_sidebar();
});
}
make_body() {
this.$body = this.$parent.find('.layout-main-section');
}
update_sidebar() {
const route = frappe.get_route_str();
const $sidebar_item = this.$sidebar.find(`[data-route="${route}"]`);
const $siblings = this.$sidebar.find('[data-route]');
$siblings.removeClass('active').addClass('text-muted');
$sidebar_item.addClass('active').removeClass('text-muted');
}
refresh() {
const route = frappe.get_route();
this.subpages = this.subpages || {};
for (let page in this.subpages) {
this.subpages[page].hide();
}
if (route[1] === 'home' && !this.subpages.home) {
this.subpages.home = new erpnext.hub.Home(this.$body);
}
if (route[1] === 'favourites' && !this.subpages.favourites) {
this.subpages.favourites = new erpnext.hub.Favourites(this.$body);
}
if (route[1] === 'category' && route[2] && !this.subpages.category) {
this.subpages.category = new erpnext.hub.Category(this.$body);
}
if (route[1] === 'item' && route[2] && !this.subpages.item) {
this.subpages.item = new erpnext.hub.Item(this.$body);
}
if (!Object.keys(this.subpages).includes(route[1])) {
frappe.show_not_found();
return;
}
this.update_sidebar();
frappe.utils.scroll_to(0);
this.subpages[route[1]].show();
}
}
class SubPage {
constructor(parent) {
this.$parent = $(parent);
this.make_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();
}
show() {
this.refresh();
this.$wrapper.show();
}
hide() {
this.$wrapper.hide();
}
}
erpnext.hub.Home = class Home extends SubPage {
make_wrapper() {
super.make_wrapper();
this.make_search_bar();
}
refresh() {
this.get_items_and_render();
}
get_items_and_render() {
this.get_items()
.then(r => {
erpnext.hub.hub_item_cache = r.message;
this.render(r.message);
});
}
get_items() {
return frappe.call('erpnext.hub_node.get_list', {
doctype: 'Hub Item',
filters: {
image: ['like', 'http%']
}
});
}
make_search_bar() {
const $search = $(`
<div class="hub-search-container">
<input type="text" class="form-control" placeholder="Search for anything">
</div>`
);
this.$wrapper.append($search);
const $search_input = $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.get_items_and_render();
}
}, 300));
}
render(items) {
const html = get_item_card_container_html(items, __('Recently Published'));
this.$wrapper.html(html)
}
}
erpnext.hub.Favourites = class Favourites extends SubPage {
refresh() {
this.get_favourites()
.then(r => {
this.render(r.message);
});
}
get_favourites() {
return frappe.call('erpnext.hub_node.get_item_favourites');
}
render(items) {
const html = get_item_card_container_html(items, __('Favourites'));
this.$wrapper.html(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) {
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.html(html)
}
}
erpnext.hub.Item = class Item extends SubPage {
refresh() {
const hub_item_code = frappe.get_route()[2];
this.get_item(hub_item_code)
.then(item => {
this.render(item);
});
}
get_item(hub_item_code) {
return new Promise(resolve => {
const item = (erpnext.hub.hub_item_cache || []).find(item => item.name === hub_item_code)
if (item) {
resolve(item);
} else {
frappe.call('erpnext.hub_node.get_list', {
doctype: 'Hub Item',
filters: {
name: hub_item_code
}
})
.then(r => {
resolve(r.message[0]);
});
}
});
}
render(item) {
const title = item.item_name || item.name;
const company = item.company_name;
const who = __('Posted By {0}', [company]);
const when = comment_when(item.creation);
const city = item.seller_city ? item.seller_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);
const rating_count = item.reviews.length > 0 ? `(${item.reviews.length} reviews)` : '';
const html = `
<div class="hub-item-container">
<div class="row">
<div class="col-md-3">
<div class="hub-item-image">
<img src="${item.image}">
</div>
</div>
<div class="col-md-6">
<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>
` : __('No description')
}
</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-6">
<a href="#marketplace/seller/${company}" class="bold">${company}</a>
<p class="text-muted">
Contact Seller
</p>
</div>
</div>
</div>
`;
this.$wrapper.html(html);
}
}
function get_item_card_container_html(items, title) {
const html = (items || []).map(item => get_item_card_html(item)).join('');
return `
<div class="row hub-card-container">
<div class="col-md-12 margin-bottom">
<b>${title}</b>
</div>
${html}
</div>
`;
}
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_name;
const route = `marketplace/item/${item.hub_item_code}`;
let subtitle = [comment_when(item.creation)];
const rating = get_rating(item);
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="${route}">
<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 ${item.image ? '' : 'no-image'}" src="${img_url}" />
<div class="hub-card-overlay"></div>
</div>
</div>
</div>
`;
return item_html;
}
function get_rating(item) {
const review_length = (item.reviews || []).length;
return review_length
? item.reviews
.map(r => r.rating)
.reduce((a, b) => a + b, 0) / review_length
: 0;
}
function get_rating_html(item) {
const rating = get_rating(item);
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;
}
erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList { erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
setup_defaults() { setup_defaults() {
super.setup_defaults(); super.setup_defaults();
@ -40,7 +439,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
get_meta() { get_meta() {
return new Promise(resolve => return new Promise(resolve =>
frappe.call('erpnext.hub_node.get_meta', {doctype: this.doctype}, resolve)); frappe.call('erpnext.hub_node.get_meta', { doctype: this.doctype }, resolve));
} }
set_breadcrumbs() { } set_breadcrumbs() { }
@ -83,7 +482,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
} }
setup_view() { setup_view() {
if(frappe.route_options){ if (frappe.route_options) {
const filters = []; const filters = [];
for (let field in frappe.route_options) { for (let field in frappe.route_options) {
var value = frappe.route_options[field]; var value = frappe.route_options[field];
@ -146,9 +545,9 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
<span class='indicator red'> <span class='indicator red'>
{{ _("Payment Cancelled") }}</span> {{ _("Payment Cancelled") }}</span>
</div> </div>
<p>${ __("Your payment is cancelled.") }</p> <p>${ __("Your payment is cancelled.")}</p>
<div><a href='' class='btn btn-primary btn-sm'> <div><a href='' class='btn btn-primary btn-sm'>
${ __("Continue") }</a></div> ${ __("Continue")}</a></div>
</div>`; </div>`;
let page = this.page.wrapper.find('.layout-side-section') let page = this.page.wrapper.find('.layout-side-section')
@ -172,7 +571,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
`); `);
} }
if(this.data.length) { if (this.data.length) {
this.doc = this.data[0]; this.doc = this.data[0];
} }
@ -226,11 +625,11 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
} }
get_image_html(encoded_name, src, alt_text) { get_image_html(encoded_name, src, alt_text) {
return `<img data-name="${encoded_name}" src="${ src }" alt="${ alt_text }">`; return `<img data-name="${encoded_name}" src="${src}" alt="${alt_text}">`;
} }
get_image_placeholder(title) { get_image_placeholder(title) {
return `<span class="placeholder-text">${ frappe.get_abbr(title) }</span>`; return `<span class="placeholder-text">${frappe.get_abbr(title)}</span>`;
} }
loadImage(item) { loadImage(item) {
@ -241,7 +640,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
let placeholder = this.get_image_placeholder(title); let placeholder = this.get_image_placeholder(title);
let $container = this.$result.find(`.image-field[data-name="${encoded_name}"]`); let $container = this.$result.find(`.image-field[data-name="${encoded_name}"]`);
if(!item[this.imageFieldName]) { if (!item[this.imageFieldName]) {
$container.prepend(placeholder); $container.prepend(placeholder);
$container.addClass('no-image'); $container.addClass('no-image');
} }
@ -262,7 +661,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
} }
setup_quick_view() { setup_quick_view() {
if(this.quick_view) return; if (this.quick_view) return;
this.quick_view = new frappe.ui.Dialog({ this.quick_view = new frappe.ui.Dialog({
title: 'Quick View', title: 'Quick View',
@ -312,10 +711,10 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
} }
setup_like() { setup_like() {
if(this.setup_like_done) return; if (this.setup_like_done) return;
this.setup_like_done = 1; this.setup_like_done = 1;
this.$result.on('click', '.btn.like-button', (e) => { this.$result.on('click', '.btn.like-button', (e) => {
if($(e.target).hasClass('changing')) return; if ($(e.target).hasClass('changing')) return;
$(e.target).addClass('changing'); $(e.target).addClass('changing');
e.preventDefault(); e.preventDefault();
@ -326,13 +725,13 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
let values = this.data_dict[name]; let values = this.data_dict[name];
let heart = $(e.target); let heart = $(e.target);
if(heart.hasClass('like-button')) { if (heart.hasClass('like-button')) {
heart = $(e.target).find('.octicon'); heart = $(e.target).find('.octicon');
} }
let remove = 1; let remove = 1;
if(heart.hasClass('liked')) { if (heart.hasClass('liked')) {
// unlike // unlike
heart.removeClass('liked'); heart.removeClass('liked');
} else { } else {
@ -349,7 +748,7 @@ erpnext.hub.HubListing = class HubListing extends frappe.views.BaseList {
}, },
callback: (r) => { callback: (r) => {
let message = __("Added to Favourites"); let message = __("Added to Favourites");
if(remove) { if (remove) {
message = __("Removed from Favourites"); message = __("Removed from Favourites");
} }
frappe.show_alert(message); frappe.show_alert(message);
@ -454,7 +853,7 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing {
label: 'All Categories', label: 'All Categories',
expandable: true, expandable: true,
args: {parent: this.current_category}, args: { parent: this.current_category },
method: 'erpnext.hub_node.get_categories', method: 'erpnext.hub_node.get_categories',
on_click: (node) => { on_click: (node) => {
this.update_category(node.label); this.update_category(node.label);
@ -501,8 +900,8 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing {
.then(r => { .then(r => {
const categories = r.message.map(d => d.value).sort(); const categories = r.message.map(d => d.value).sort();
const sidebar_items = [ const sidebar_items = [
`<li class="hub-sidebar-item bold is-title"> `<li class="hub-sidebar-item bold text-muted is-title">
Category ${__('Category')}
</li>`, </li>`,
`<li class="hub-sidebar-item active"> `<li class="hub-sidebar-item active">
All All
@ -523,7 +922,7 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing {
} }
update_category(label) { update_category(label) {
this.current_category = (label=='All Categories') ? undefined : label; this.current_category = (label == 'All Categories') ? undefined : label;
this.refresh(); this.refresh();
} }
@ -573,14 +972,14 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing {
const ratingAverage = reviewLength const ratingAverage = reviewLength
? item.reviews ? item.reviews
.map(r => r.rating) .map(r => r.rating)
.reduce((a, b) => a + b, 0)/reviewLength .reduce((a, b) => a + b, 0) / reviewLength
: -1; : -1;
let ratingHtml = ``; let ratingHtml = ``;
for(var i = 0; i < 5; i++) { for (var i = 0; i < 5; i++) {
let starClass = 'fa-star'; let starClass = 'fa-star';
if(i >= ratingAverage) starClass = 'fa-star-o'; if (i >= ratingAverage) starClass = 'fa-star-o';
ratingHtml += `<i class='fa fa-fw ${starClass} star-icon' data-index=${i}></i>`; ratingHtml += `<i class='fa fa-fw ${starClass} star-icon' data-index=${i}></i>`;
} }
let dot_spacer = '<span aria-hidden="true"> · </span>'; let dot_spacer = '<span aria-hidden="true"> · </span>';
@ -608,7 +1007,7 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing {
(${reviewLength}) (${reviewLength})
</div> </div>
<div class="list-row-col"> <div class="list-row-col">
<a href="${'#Hub/Company/'+company_name+'/Items'}"><p>${ company_name }</p></a> <a href="${'#Hub/Company/' + company_name + '/Items'}"><p>${company_name}</p></a>
</div> </div>
</div> </div>
<div class="hub-card-body"> <div class="hub-card-body">
@ -651,7 +1050,7 @@ erpnext.hub.ItemListing = class ItemListing extends erpnext.hub.HubListing {
}; };
erpnext.hub.Favourites = class Favourites extends erpnext.hub.ItemListing { erpnext.hub.Favourites2 = class Favourites extends erpnext.hub.ItemListing {
constructor(opts) { constructor(opts) {
super(opts); super(opts);
this.show(); this.show();
@ -702,18 +1101,18 @@ erpnext.hub.Favourites = class Favourites extends erpnext.hub.ItemListing {
} }
update_category(label) { update_category(label) {
this.current_category = (label=='All Categories') ? undefined : label; this.current_category = (label == 'All Categories') ? undefined : label;
this.refresh(); this.refresh();
} }
get_filters_for_args() { get_filters_for_args() {
if(!this.filter_area) return; if (!this.filter_area) return;
let filters = {}; let filters = {};
this.filter_area.get().forEach(f => { this.filter_area.get().forEach(f => {
let field = f[1] !== 'name' ? f[1] : 'item_name'; let field = f[1] !== 'name' ? f[1] : 'item_name';
filters[field] = [f[2], f[3]]; filters[field] = [f[2], f[3]];
}); });
if(this.current_category) { if (this.current_category) {
filters['hub_category'] = this.current_category; filters['hub_category'] = this.current_category;
} }
return filters; return filters;
@ -772,10 +1171,10 @@ erpnext.hub.CompanyListing = class CompanyListing extends erpnext.hub.HubListing
get_filters_for_args() { get_filters_for_args() {
let filters = {}; let filters = {};
this.filter_area.get().forEach(f => { // this.filter_area.get().forEach(f => {
let field = f[1] !== 'name' ? f[1] : 'company_name'; // let field = f[1] !== 'name' ? f[1] : 'company_name';
filters[field] = [f[2], f[3]]; // filters[field] = [f[2], f[3]];
}); // });
return filters; return filters;
} }

View File

@ -1,18 +1,15 @@
@import "../../../../frappe/frappe/public/less/variables.less"; @import "../../../../frappe/frappe/public/less/variables.less";
body[data-route^="Hub/"] { body[data-route^="marketplace/"] {
.hub-icon { .layout-side-section {
width: 40px; padding-top: 25px;
height: 40px; padding-right: 25px;
} }
.layout-main-section { .layout-main-section {
border: none; border: none;
}
.frappe-list {
padding-top: 25px;
font-size: @text-medium; font-size: @text-medium;
padding-top: 25px;
@media (max-width: @screen-xs) { @media (max-width: @screen-xs) {
padding-left: 20px; padding-left: 20px;
@ -26,17 +23,32 @@ body[data-route^="Hub/"] {
border-radius: 4px; border-radius: 4px;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
&:hover .hub-card-overlay {
display: block;
}
} }
.hub-card-header { .hub-card-header {
padding: 12px 15px; padding: 12px 15px;
height: 60px; height: 60px;
border-bottom: 1px solid @border-color;
} }
.hub-card-body { .hub-card-body {
position: relative;
height: 200px; height: 200px;
} }
.hub-card-overlay {
display: none;
position: absolute;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.1);
}
.hub-card-image { .hub-card-image {
min-width: 100%; min-width: 100%;
width: 100%; width: 100%;
@ -73,6 +85,30 @@ body[data-route^="Hub/"] {
} }
} }
.hub-item-image {
border: 1px solid @border-color;
border-radius: 4px;
overflow: hidden;
height: 200px;
width: 200px;
display: flex;
align-items: center;
}
.hub-item-seller img {
width: 50px;
height: 50px;
border-radius: 4px;
border: 1px solid @border-color;
}
}
body[data-route^="Hub/"] {
.hub-icon {
width: 40px;
height: 40px;
}
.img-wrapper { .img-wrapper {
border: 1px solid #d1d8dd; border: 1px solid #d1d8dd;
border-radius: 3px; border-radius: 3px;