chore: UI refresh for grid/list view and search

- enhanced UI for grid/list view, actions visible on hover only
- enhanced search UI
- Added indicator to show if item is in cart
- Moved search with view togglers
This commit is contained in:
marination
2021-07-12 03:28:33 +05:30
parent 9920747a26
commit cf88b517c8
14 changed files with 439 additions and 195 deletions

View File

@@ -61,7 +61,7 @@ def add_item_review(web_item, title, rating, comment=None):
doc.published_on = datetime.today().strftime("%d %B %Y") doc.published_on = datetime.today().strftime("%d %B %Y")
doc.insert() doc.insert()
def get_customer(): def get_customer(silent=False):
user = frappe.session.user user = frappe.session.user
contact_name = get_contact_name(user) contact_name = get_contact_name(user)
customer = None customer = None
@@ -75,5 +75,7 @@ def get_customer():
if customer: if customer:
return frappe.db.get_value("Customer", customer) return frappe.db.get_value("Customer", customer)
elif silent:
return None
else: else:
frappe.throw(_("You are not verified to write a review yet. Please contact us for verification.")) frappe.throw(_("You are not verified to write a review yet. Please contact us for verification."))

View File

@@ -310,7 +310,7 @@
{ {
"description": "Short Description for List View", "description": "Short Description for List View",
"fieldname": "short_description", "fieldname": "short_description",
"fieldtype": "Data", "fieldtype": "Small Text",
"label": "Short Website Description" "label": "Short Website Description"
} }
], ],
@@ -318,7 +318,7 @@
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-07-08 19:25:15.115746", "modified": "2021-07-11 20:49:45.415421",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "E-commerce", "module": "E-commerce",
"name": "Website Item", "name": "Website Item",

View File

@@ -22,7 +22,7 @@ erpnext.ProductGrid = class {
this.items.forEach(item => { this.items.forEach(item => {
let title = item.web_item_name || item.item_name || item.item_code || ""; let title = item.web_item_name || item.item_name || item.item_code || "";
title = title.length > 50 ? title.substr(0, 50) + "..." : title; title = title.length > 90 ? title.substr(0, 90) + "..." : title;
html += `<div class="col-sm-4 item-card"><div class="card text-left">`; html += `<div class="col-sm-4 item-card"><div class="card text-left">`;
html += me.get_image_html(item, title); html += me.get_image_html(item, title);
@@ -60,13 +60,20 @@ erpnext.ProductGrid = class {
get_card_body_html(item, title, settings) { get_card_body_html(item, title, settings) {
let body_html = ` let body_html = `
<div class="card-body text-left" style="width:100%"> <div class="card-body text-left card-body-flex" style="width:100%">
<div style="margin-top: 16px; display: flex;"> <div style="margin-top: 16px; display: flex;">
`; `;
body_html += this.get_title_with_indicator(item, title, settings); body_html += this.get_title(item, title);
// get floating elements
if (!item.has_variants) {
if (settings.enable_wishlist) {
body_html += this.get_wishlist_icon(item);
}
if (settings.enabled) {
body_html += this.get_cart_indicator(item);
}
if (!item.has_variants && settings.enable_wishlist) {
body_html += this.get_wishlist_icon(item);
} }
body_html += `</div>`; // close div on line 50 body_html += `</div>`; // close div on line 50
@@ -76,29 +83,28 @@ erpnext.ProductGrid = class {
body_html += this.get_price_html(item); body_html += this.get_price_html(item);
} }
body_html += this.get_stock_availability(item, settings);
body_html += this.get_primary_button(item, settings); body_html += this.get_primary_button(item, settings);
body_html += `</div>`; // close div on line 49 body_html += `</div>`; // close div on line 49
return body_html; return body_html;
} }
get_title_with_indicator(item, title, settings) { get_title(item, title) {
let title_html = ` let title_html = `
<a href="/${ item.route || '#' }"> <a href="/${ item.route || '#' }">
<div class="product-title"> <div class="product-title">
${ title || '' } ${ title || '' }
</div>
</a>
`; `;
if (item.in_stock && settings.show_stock_availability) {
title_html += `<span class="indicator ${ item.in_stock } card-indicator"></span>`;
}
title_html += `</div></a>`;
return title_html; return title_html;
} }
get_wishlist_icon(item) { get_wishlist_icon(item) {
let icon_class = item.wished ? "wished" : "not-wished"; let icon_class = item.wished ? "wished" : "not-wished";
return ` return `
<div class="like-action" <div class="like-action ${ item.wished ? "like-action-wished" : ''}"
data-item-code="${ item.item_code }" data-item-code="${ item.item_code }"
data-price="${ item.price || '' }" data-price="${ item.price || '' }"
data-formatted-price="${ item.formatted_price || '' }"> data-formatted-price="${ item.formatted_price || '' }">
@@ -109,6 +115,14 @@ erpnext.ProductGrid = class {
`; `;
} }
get_cart_indicator(item) {
return `
<div class="cart-indicator ${item.in_cart ? '' : 'hidden'}" data-item-code="${ item.item_code }">
1
</div>
`;
}
get_price_html(item) { get_price_html(item) {
let price_html = ` let price_html = `
<div class="product-price"> <div class="product-price">
@@ -117,10 +131,10 @@ erpnext.ProductGrid = class {
if (item.formatted_mrp) { if (item.formatted_mrp) {
price_html += ` price_html += `
<small class="ml-1 text-muted"> <small class="striked-price">
<s>${ item.formatted_mrp }</s> <s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" }</s>
</small> </small>
<small class="ml-1" style="color: #F47A7A; font-weight: 500;"> <small class="ml-1 product-info-green">
${ item.discount } OFF ${ item.discount } OFF
</small> </small>
`; `;
@@ -129,6 +143,13 @@ erpnext.ProductGrid = class {
return price_html; return price_html;
} }
get_stock_availability(item, settings) {
if (!item.has_variants && !item.in_stock && settings.show_stock_availability) {
return `<span class="out-of-stock">Out of stock</span>`;
}
return ``;
}
get_primary_button(item, settings) { get_primary_button(item, settings) {
if (item.has_variants) { if (item.has_variants) {
return ` return `
@@ -138,13 +159,29 @@ erpnext.ProductGrid = class {
</div> </div>
</a> </a>
`; `;
} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock !== "red")) { } else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) {
return ` return `
<div id="${ item.name }" class="btn <div id="${ item.name }" class="btn
btn-sm btn-add-to-cart-list not-added w-100 mt-4" btn-sm btn-primary btn-add-to-cart-list
w-100 mt-4 ${ item.in_cart ? 'hidden' : '' }"
data-item-code="${ item.item_code }"> data-item-code="${ item.item_code }">
<span class="mr-2">
<svg class="icon icon-md">
<use href="#icon-assets"></use>
</svg>
</span>
${ __('Add to Cart') } ${ __('Add to Cart') }
</div> </div>
<a href="/cart">
<div id="${ item.name }" class="btn
btn-sm btn-primary btn-add-to-cart-list
w-100 mt-4 go-to-cart-grid
${ item.in_cart ? '' : 'hidden' }"
data-item-code="${ item.item_code }">
${ __('Go to Cart') }
</div>
</a>
`; `;
} else { } else {
return ``; return ``;

View File

@@ -24,8 +24,8 @@ erpnext.ProductList = class {
let title = item.web_item_name || item.item_name || item.item_code || ""; let title = item.web_item_name || item.item_name || item.item_code || "";
title = title.length > 200 ? title.substr(0, 200) + "..." : title; title = title.length > 200 ? title.substr(0, 200) + "..." : title;
html += `<div class='row mt-6 w-100' style="border-bottom: 1px solid var(--table-border-color); padding-bottom: 1rem;">`; html += `<div class='row list-row w-100'>`;
html += me.get_image_html(item, title); html += me.get_image_html(item, title, me.settings);
html += me.get_row_body_html(item, title, me.settings); html += me.get_row_body_html(item, title, me.settings);
html += `</div>`; html += `</div>`;
}); });
@@ -34,20 +34,23 @@ erpnext.ProductList = class {
$product_wrapper.append(html); $product_wrapper.append(html);
} }
get_image_html(item, title) { get_image_html(item, title, settings) {
let image = item.website_image || item.image; let image = item.website_image || item.image;
let wishlist_enabled = !item.has_variants && settings.enable_wishlist;
let image_html = ``;
if (image) { if (image) {
return ` image_html += `
<div class="col-2 border text-center rounded list-image"> <div class="col-2 border text-center rounded list-image">
<a class="product-link product-list-link" href="/${ item.route || '#' }"> <a class="product-link product-list-link" href="/${ item.route || '#' }">
<img itemprop="image" class="website-image h-100 w-100" alt="${ title }" <img itemprop="image" class="website-image h-100 w-100" alt="${ title }"
src="${ image }"> src="${ image }">
</a> </a>
${ wishlist_enabled ? this.get_wishlist_icon(item): '' }
</div> </div>
`; `;
} else { } else {
return ` image_html += `
<div class="col-2 border text-center rounded list-image"> <div class="col-2 border text-center rounded list-image">
<a class="product-link product-list-link" href="/${ item.route || '#' }" <a class="product-link product-list-link" href="/${ item.route || '#' }"
style="text-decoration: none"> style="text-decoration: none">
@@ -55,13 +58,16 @@ erpnext.ProductList = class {
${ frappe.get_abbr(title) } ${ frappe.get_abbr(title) }
</div> </div>
</a> </a>
${ wishlist_enabled ? this.get_wishlist_icon(item): '' }
</div> </div>
`; `;
} }
return image_html;
} }
get_row_body_html(item, title, settings) { get_row_body_html(item, title, settings) {
let body_html = `<div class='col-9 text-left'>`; let body_html = `<div class='col-10 text-left'>`;
body_html += this.get_title_html(item, title, settings); body_html += this.get_title_html(item, title, settings);
body_html += this.get_item_details(item, settings); body_html += this.get_item_details(item, settings);
body_html += `</div>`; body_html += `</div>`;
@@ -76,18 +82,11 @@ erpnext.ProductList = class {
style="color: var(--gray-800); font-weight: 500;"> style="color: var(--gray-800); font-weight: 500;">
${ title } ${ title }
</a> </a>
</div>
`; `;
if (item.in_stock && settings.show_stock_availability) { if (settings.enabled) {
title_html += `<span class="indicator ${ item.in_stock } card-indicator"></span>`;
}
title_html += `</div>`;
if (settings.enable_wishlist || settings.enabled) {
title_html += `<div class="col-4" style="display:flex">`; title_html += `<div class="col-4" style="display:flex">`;
if (!item.has_variants && settings.enable_wishlist) {
title_html += this.get_wishlist_icon(item);
}
title_html += this.get_primary_button(item, settings); title_html += this.get_primary_button(item, settings);
title_html += `</div>`; title_html += `</div>`;
} }
@@ -96,12 +95,12 @@ erpnext.ProductList = class {
return title_html; return title_html;
} }
get_item_details(item) { get_item_details(item, settings) {
let details = ` let details = `
<p class="product-code"> <p class="product-code">
Item Code : ${ item.item_code } ${ item.item_group } | Item Code : ${ item.item_code }
</p> </p>
<div class="text-muted mt-2"> <div class="mt-2" style="color: var(--gray-600) !important; font-size: 13px;">
${ item.short_description || '' } ${ item.short_description || '' }
</div> </div>
<div class="product-price"> <div class="product-price">
@@ -110,24 +109,33 @@ erpnext.ProductList = class {
if (item.formatted_mrp) { if (item.formatted_mrp) {
details += ` details += `
<small class="ml-1 text-muted"> <small class="striked-price">
<s>${ item.formatted_mrp }</s> <s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" }</s>
</small> </small>
<small class="ml-1" style="color: #F47A7A; font-weight: 500;"> <small class="ml-1 product-info-green">
${ item.discount } OFF ${ item.discount } OFF
</small> </small>
`; `;
} }
details += this.get_stock_availability(item, settings);
details += `</div>`; details += `</div>`;
return details; return details;
} }
get_stock_availability(item, settings) {
if (!item.has_variants && !item.in_stock && settings.show_stock_availability) {
return `<br><span class="out-of-stock mt-2">Out of stock</span>`;
}
return ``;
}
get_wishlist_icon(item) { get_wishlist_icon(item) {
let icon_class = item.wished ? "wished" : "not-wished"; let icon_class = item.wished ? "wished" : "not-wished";
return ` return `
<div class="like-action mr-4" <div class="like-action-list ${ item.wished ? "like-action-wished" : ''}"
data-item-code="${ item.item_code }" data-item-code="${ item.item_code }"
data-price="${ item.price || '' }" data-price="${ item.price || '' }"
data-formatted-price="${ item.formatted_price || '' }"> data-formatted-price="${ item.formatted_price || '' }">
@@ -142,19 +150,43 @@ erpnext.ProductList = class {
if (item.has_variants) { if (item.has_variants) {
return ` return `
<a href="/${ item.route || '#' }"> <a href="/${ item.route || '#' }">
<div class="btn btn-sm btn-explore-variants" style="margin-bottom: 0; margin-top: 4px; max-height: 30px;"> <div class="btn btn-sm btn-explore-variants btn"
style="margin-bottom: 0; max-height: 30px; float: right;
padding: 0.25rem 1rem; min-width: 135px;">
${ __('Explore') } ${ __('Explore') }
</div> </div>
</a> </a>
`; `;
} else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock !== "red")) { } else if (settings.enabled && (settings.allow_items_not_in_stock || item.in_stock)) {
return ` return `
<div id="${ item.name }" class="btn <div id="${ item.name }" class="btn
btn-sm btn-add-to-cart-list not-added" btn-sm btn-primary btn-add-to-cart-list
${ item.in_cart ? 'hidden' : '' }"
data-item-code="${ item.item_code }" data-item-code="${ item.item_code }"
style="margin-bottom: 0; margin-top: 0px; max-height: 30px;"> style="margin-bottom: 0; margin-top: 0px !important; max-height: 30px; float: right;
padding: 0.25rem 1rem; min-width: 135px;">
<span class="mr-2">
<svg class="icon icon-md">
<use href="#icon-assets"></use>
</svg>
</span>
${ __('Add to Cart') } ${ __('Add to Cart') }
</div> </div>
<div class="cart-indicator ${item.in_cart ? '' : 'hidden'}" style="position: unset;">
1
</div>
<a href="/cart">
<div id="${ item.name }" class="btn
btn-sm btn-primary btn-add-to-cart-list
ml-4 go-to-cart
${ item.in_cart ? '' : 'hidden' }"
data-item-code="${ item.item_code }"
style="padding: 0.25rem 1rem; min-width: 135px;">
${ __('Go to Cart') }
</div>
</a>
`; `;
} else { } else {
return ``; return ``;

View File

@@ -3,6 +3,7 @@
import frappe import frappe
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
from erpnext.e_commerce.doctype.item_review.item_review import get_customer
from frappe.utils import flt from frappe.utils import flt
class ProductQuery: class ProductQuery:
@@ -38,7 +39,7 @@ class ProductQuery:
""" """
# track if discounts included in field filters # track if discounts included in field filters
self.filter_with_discount = bool(fields.get("discount")) self.filter_with_discount = bool(fields.get("discount"))
result, discount_list, website_item_groups, count = [], [], [], 0 result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0
website_item_groups = self.get_website_item_group_results(item_group, website_item_groups) website_item_groups = self.get_website_item_group_results(item_group, website_item_groups)
@@ -59,7 +60,11 @@ class ProductQuery:
# sort combined results by ranking # sort combined results by ranking
result = sorted(result, key=lambda x: x.get("ranking"), reverse=True) result = sorted(result, key=lambda x: x.get("ranking"), reverse=True)
result, discount_list = self.add_display_details(result, discount_list)
if self.settings.enabled:
cart_items = self.get_cart_items()
result, discount_list = self.add_display_details(result, discount_list, cart_items)
discounts = [] discounts = []
if discount_list: if discount_list:
@@ -191,7 +196,7 @@ class ProductQuery:
) )
return website_item_groups return website_item_groups
def add_display_details(self, result, discount_list): def add_display_details(self, result, discount_list, cart_items):
"""Add price and availability details in result.""" """Add price and availability details in result."""
for item in result: for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
@@ -203,6 +208,8 @@ class ProductQuery:
if self.settings.show_stock_availability: if self.settings.show_stock_availability:
self.get_stock_availability(item) self.get_stock_availability(item)
item.in_cart = item.item_code in cart_items
item.wished = False item.wished = False
if frappe.db.exists("Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}): if frappe.db.exists("Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}):
item.wished = True item.wished = True
@@ -225,13 +232,31 @@ class ProductQuery:
def get_stock_availability(self, item): def get_stock_availability(self, item):
"""Modify item object and add stock details.""" """Modify item object and add stock details."""
item.in_stock = False
if item.get("website_warehouse"): if item.get("website_warehouse"):
stock_qty = frappe.utils.flt( stock_qty = frappe.utils.flt(
frappe.db.get_value("Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")}, frappe.db.get_value("Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")},
"actual_qty")) "actual_qty"))
item.in_stock = "green" if stock_qty else "red" item.in_stock = bool(stock_qty)
elif not frappe.db.get_value("Item", item.item_code, "is_stock_item"):
item.in_stock = "green" # non-stock item will always be available def get_cart_items(self):
customer = get_customer(silent=True)
if customer:
quotation = frappe.get_all("Quotation", fields=["name"], filters=
{"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
if quotation:
items = frappe.get_all(
"Quotation Item",
fields=["item_code"],
filters={
"parent": quotation[0].get("name")
})
items = [row.item_code for row in items]
return items
return []
def combine_web_item_group_results(self, item_group, result, website_item_groups): def combine_web_item_group_results(self, item_group, result, website_item_groups):
"""Combine results with context of website item groups into item results.""" """Combine results with context of website item groups into item results."""

View File

@@ -10,8 +10,6 @@ erpnext.ProductSearch = class {
setupSearchDropDown() { setupSearchDropDown() {
this.search_area = $("#dropdownMenuSearch"); this.search_area = $("#dropdownMenuSearch");
this.setupSearchResultContainer(); this.setupSearchResultContainer();
this.setupProductsContainer();
this.setupCategoryRecentsContainer();
this.populateRecentSearches(); this.populateRecentSearches();
} }
@@ -82,60 +80,46 @@ erpnext.ProductSearch = class {
<div class="overflow-hidden shadow dropdown-menu w-100 hidden" <div class="overflow-hidden shadow dropdown-menu w-100 hidden"
id="search-results-container" id="search-results-container"
aria-labelledby="dropdownMenuSearch" aria-labelledby="dropdownMenuSearch"
style="display: flex;"> style="display: flex; flex-direction: column;">
</div> </div>
`).find("#search-results-container"); `).find("#search-results-container");
this.setupCategoryContainer()
this.setupProductsContainer();
this.setupRecentsContainer();
} }
setupProductsContainer() { setupProductsContainer() {
let $products_section = this.search_dropdown.append(` this.products_container = this.search_dropdown.append(`
<div class="col-7 mr-2 mt-1" <div id="product-results mt-2">
id="product-results" <div id="product-scroll" style="overflow: scroll; max-height: 300px">
style="border-right: 1px solid var(--gray-200);">
</div>
`).find("#product-results");
this.products_container = $products_section.append(`
<div id="product-scroll" style="overflow: scroll; max-height: 300px">
<div class="mt-6 w-100 text-muted" style="font-weight: 400; text-align: center;">
${ __("Type something ...") }
</div> </div>
</div> </div>
`).find("#product-scroll"); `).find("#product-scroll");
} }
setupCategoryRecentsContainer() { setupCategoryContainer() {
let $category_recents_section = $("#search-results-container").append(` this.category_container = this.search_dropdown.append(`
<div id="category-recents-container" <div class="category-container mt-2 mb-1">
class="col-5 mt-2 h-100" <div class="category-chips">
style="margin-left: -15px;">
</div>
`).find("#category-recents-container");
this.category_container = $category_recents_section.append(`
<div class="category-container">
<div class="mb-2"
style="border-bottom: 1px solid var(--gray-200);">
${ __("Categories") }
</div>
<div class="categories">
<span class="text-muted" style="font-weight: 400;"> ${ __('No results') } <span>
</div> </div>
</div> </div>
`).find(".categories"); `).find(".category-chips");
}
let $recents_section = $("#category-recents-container").append(` setupRecentsContainer() {
<div class="mb-2 mt-4 recent-searches"> let $recents_section = this.search_dropdown.append(`
<div style="border-bottom: 1px solid var(--gray-200);"> <div class="mb-2 mt-2 recent-searches">
${ __("Recent") } <div>
<b>${ __("Recent") }</b>
</div> </div>
</div> </div>
`).find(".recent-searches"); `).find(".recent-searches");
this.recents_container = $recents_section.append(` this.recents_container = $recents_section.append(`
<div id="recent-chips" style="padding: 1rem 0;"> <div id="recents" style="padding: .25rem 0 1rem 0;">
</div> </div>
`).find("#recent-chips"); `).find("#recents");
} }
getRecentSearches() { getRecentSearches() {
@@ -144,12 +128,12 @@ erpnext.ProductSearch = class {
attachEventListenersToChips() { attachEventListenersToChips() {
let me = this; let me = this;
const chips = $(".recent-chip"); const chips = $(".recent-search");
window.chips = chips; window.chips = chips;
for (let chip of chips) { for (let chip of chips) {
chip.addEventListener("click", () => { chip.addEventListener("click", () => {
me.searchBox[0].value = chip.innerText; me.searchBox[0].value = chip.innerText.trim();
// Start search with `recent query` // Start search with `recent query`
me.searchBox.trigger("input"); me.searchBox.trigger("input");
@@ -179,15 +163,22 @@ erpnext.ProductSearch = class {
let recents = this.getRecentSearches(); let recents = this.getRecentSearches();
if (!recents.length) { if (!recents.length) {
this.recents_container.html(`<span class=""text-muted">No searches yet.</span>`);
return; return;
} }
let html = ""; let html = "";
recents.forEach((key) => { recents.forEach((key) => {
html += ` html += `
<button class="btn btn-sm recent-chip mr-1 mb-2" style="font-size: 13px"> <div class="recent-search mr-1" style="font-size: 13px">
<span class="mr-2">
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14Z" stroke="var(--gray-500)"" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.00027 5.20947V8.00017L10 10" stroke="var(--gray-500)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
${ key } ${ key }
</button> </div>
`; `;
}); });
@@ -197,11 +188,7 @@ erpnext.ProductSearch = class {
populateResults(data) { populateResults(data) {
if (data.length === 0 || data.message.results.length === 0) { if (data.length === 0 || data.message.results.length === 0) {
let empty_html = ` let empty_html = ``;
<div class="mt-6 w-100 text-muted" style="font-weight: 400; text-align: center;">
${ __('No results') }
</div>
`;
this.products_container.html(empty_html); this.products_container.html(empty_html);
return; return;
} }
@@ -228,21 +215,27 @@ erpnext.ProductSearch = class {
populateCategoriesList(data) { populateCategoriesList(data) {
if (data.length === 0 || data.message.results.length === 0) { if (data.length === 0 || data.message.results.length === 0) {
let empty_html = ` let empty_html = `
<span class="text-muted" style="font-weight: 400;"> <div class="category-container mt-2">
${__('No results')} <div class="category-chips">
</span> </div>
</div>
`; `;
this.category_container.html(empty_html); this.category_container.html(empty_html);
return; return;
} }
let html = ""; let html = `
<div class="mb-2">
<b>${ __("Categories") }</b>
</div>
`;
let search_results = data.message.results; let search_results = data.message.results;
search_results.forEach((category) => { search_results.forEach((category) => {
html += ` html += `
<div class="mb-2" style="font-weight: 400;"> <a href="/${category.route}" class="btn btn-sm category-chip mr-2 mb-2"
<a href="/${category.route}">${category.name}</a> style="font-size: 13px" role="button">
</div> ${ category.name }
</button>
`; `;
}); });

View File

@@ -12,11 +12,25 @@ erpnext.ProductView = class {
make(from_filters=false) { make(from_filters=false) {
this.products_section.empty(); this.products_section.empty();
this.prepare_view_toggler(); this.prepare_toolbar();
this.get_item_filter_data(from_filters); this.get_item_filter_data(from_filters);
} }
prepare_toolbar() {
this.products_section.append(`
<div class="toolbar d-flex">
</div>
`);
this.prepare_search();
this.prepare_view_toggler();
frappe.require('/assets/js/e-commerce.min.js', function() {
new erpnext.ProductSearch();
});
}
prepare_view_toggler() { prepare_view_toggler() {
if (!$("#list").length || !$("#image-view").length) { if (!$("#list").length || !$("#image-view").length) {
this.render_view_toggler(); this.render_view_toggler();
this.bind_view_toggler_actions(); this.bind_view_toggler_actions();
@@ -173,19 +187,45 @@ erpnext.ProductView = class {
} }
} }
prepare_search() {
$(".toolbar").append(`
<div class="input-group col-6">
<div class="dropdown w-100" id="dropdownMenuSearch">
<input type="search" name="query" id="search-box" class="form-control font-md"
placeholder="Search for Products"
aria-label="Product" aria-describedby="button-addon2">
<div class="search-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-search">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</div>
<!-- Results dropdown rendered in product_search.js -->
</div>
</div>
`);
}
render_view_toggler() { render_view_toggler() {
$(".toolbar").append(`<div class="toggle-container col-6"></div>`);
["btn-list-view", "btn-grid-view"].forEach(view => { ["btn-list-view", "btn-grid-view"].forEach(view => {
let icon = view === "btn-list-view" ? "list" : "image-view"; let icon = view === "btn-list-view" ? "list" : "image-view";
this.products_section.append(` $(".toggle-container").append(`
<div class="form-group mb-0" id="toggle-view"> <div class="form-group mb-0" id="toggle-view">
<button id="${ icon }" class="btn ${ view } mr-2"> <button id="${ icon }" class="btn ${ view } mr-2">
<span> <span>
<svg class="icon icon-md"> <svg class="icon icon-md">
<use href="#icon-${ icon }"></use> <use href="#icon-${ icon }"></use>
</svg> </svg>
</span> </span>
</button> </button>
</div>`); </div>
`);
}); });
} }
@@ -448,9 +488,6 @@ erpnext.ProductView = class {
render_item_sub_categories(categories) { render_item_sub_categories(categories) {
if (categories && categories.length) { if (categories && categories.length) {
let sub_group_html = ` let sub_group_html = `
<div class="sub-category-container">
<div class="heading"> ${ __('Sub Categories') } </div>
</div>
<div class="sub-category-container scroll-categories"> <div class="sub-category-container scroll-categories">
`; `;

View File

@@ -166,19 +166,6 @@ $.extend(shopping_cart, {
}); });
}, },
animate_add_to_cart(button) {
// Create 'added to cart' animation
let btn_id = "#" + button[0].id;
this.toggle_button_class(button, 'not-added', 'added-to-cart');
$(btn_id).text('Added to Cart');
// undo
setTimeout(() => {
this.toggle_button_class(button, 'added-to-cart', 'not-added');
$(btn_id).text('Add to Cart');
}, 2000);
},
toggle_button_class(button, remove, add) { toggle_button_class(button, remove, add) {
button.removeClass(remove); button.removeClass(remove);
button.addClass(add); button.addClass(add);
@@ -189,7 +176,10 @@ $.extend(shopping_cart, {
const $btn = $(e.currentTarget); const $btn = $(e.currentTarget);
$btn.prop('disabled', true); $btn.prop('disabled', true);
this.animate_add_to_cart($btn); $btn.addClass('hidden');
$btn.parent().find('.go-to-cart').removeClass('hidden');
$btn.parent().find('.go-to-cart-grid').removeClass('hidden');
$btn.parent().find('.cart-indicator').removeClass('hidden');
const item_code = $btn.data('item-code'); const item_code = $btn.data('item-code');
e_commerce.shopping_cart.update_cart({ e_commerce.shopping_cart.update_cart({

View File

@@ -79,7 +79,7 @@ $.extend(wishlist, {
bind_wishlist_action() { bind_wishlist_action() {
// 'wish'('like') or 'unwish' item in product listing // 'wish'('like') or 'unwish' item in product listing
$('.page_content').on('click', '.like-action', (e) => { $('.page_content').on('click', '.like-action, .like-action-list', (e) => {
const $btn = $(e.currentTarget); const $btn = $(e.currentTarget);
const $wish_icon = $btn.find('.wish-icon'); const $wish_icon = $btn.find('.wish-icon');
let me = this; let me = this;
@@ -101,6 +101,7 @@ $.extend(wishlist, {
if ($wish_icon.hasClass('wished')) { if ($wish_icon.hasClass('wished')) {
// un-wish item // un-wish item
$btn.removeClass("like-animate"); $btn.removeClass("like-animate");
$btn.addClass("like-action-wished");
this.toggle_button_class($wish_icon, 'wished', 'not-wished'); this.toggle_button_class($wish_icon, 'wished', 'not-wished');
let args = { item_code: $btn.data('item-code') }; let args = { item_code: $btn.data('item-code') };
@@ -111,6 +112,7 @@ $.extend(wishlist, {
} else { } else {
// wish item // wish item
$btn.addClass("like-animate"); $btn.addClass("like-animate");
$btn.addClass("like-action-wished");
this.toggle_button_class($wish_icon, 'not-wished', 'wished'); this.toggle_button_class($wish_icon, 'not-wished', 'wished');
let args = { let args = {

View File

@@ -1,5 +1,9 @@
@import "frappe/public/scss/common/mixins"; @import "frappe/public/scss/common/mixins";
:root {
--green-info: #38A160;
}
body.product-page { body.product-page {
background: var(--gray-50); background: var(--gray-50);
} }
@@ -70,6 +74,7 @@ body.product-page {
.item-card-group-section { .item-card-group-section {
.card { .card {
height: 100%;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -79,6 +84,19 @@ body.product-page {
} }
} }
.card:hover, .card:focus-within {
.btn-add-to-cart-list {
visibility: visible;
}
.like-action {
visibility: visible;
}
.btn-explore-variants {
visibility: visible;
}
}
.card-img-container { .card-img-container {
height: 210px; height: 210px;
width: 100%; width: 100%;
@@ -92,12 +110,10 @@ body.product-page {
.no-image { .no-image {
@include flex(flex, center, center, null); @include flex(flex, center, center, null);
height: 200px; height: 220px;
margin: 0 auto;
margin-top: var(--margin-xl);
background: var(--gray-100); background: var(--gray-100);
width: 80%; width: 100%;
border-radius: var(--border-radius); border-radius: var(--border-radius) var(--border-radius) 0 0;
font-size: 2rem; font-size: 2rem;
color: var(--gray-500); color: var(--gray-500);
} }
@@ -113,6 +129,11 @@ body.product-page {
margin-bottom: 15px; margin-bottom: 15px;
} }
.card-body-flex {
display: flex;
flex-direction: column;
}
.product-title { .product-title {
font-size: 14px; font-size: 14px;
color: var(--gray-800); color: var(--gray-800);
@@ -143,6 +164,24 @@ body.product-page {
font-weight: 600; font-weight: 600;
color: var(--text-color); color: var(--text-color);
margin: var(--margin-sm) 0; margin: var(--margin-sm) 0;
.striked-price {
font-weight: 500;
font-size: 15px;
color: var(--gray-500);
}
}
.product-info-green {
color: var(--green-info);
font-weight: 600;
}
.out-of-stock {
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #F47A7A;
} }
.item-card { .item-card {
@@ -156,6 +195,28 @@ body.product-page {
} }
} }
.list-row {
padding-bottom: 1rem;
padding-top: 1.5rem !important;
border-radius: 8px;
border-bottom: 1px solid var(--gray-50);
&:hover, &:focus-within {
box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04);
transition: box-shadow 400ms;
.btn-add-to-cart-list {
visibility: visible;
}
.like-action-list {
visibility: visible;
}
.btn-explore-variants {
visibility: visible;
}
}
}
[data-doctype="Item Group"], [data-doctype="Item Group"],
#page-all-products { #page-all-products {
.page-header { .page-header {
@@ -210,6 +271,7 @@ body.product-page {
} }
.list-image { .list-image {
border: none !important;
overflow: hidden; overflow: hidden;
max-height: 200px; max-height: 200px;
background-color: white; background-color: white;
@@ -331,7 +393,7 @@ body.product-page {
.product-code { .product-code {
color: var(--text-muted); color: var(--text-muted);
font-size: 13px; font-size: 14px;
} }
.item-configurator-dialog { .item-configurator-dialog {
@@ -381,7 +443,7 @@ body.product-page {
} }
.sub-category-container { .sub-category-container {
padding-bottom: 1rem; padding-bottom: .5rem;
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
border-bottom: 1px solid var(--table-border-color); border-bottom: 1px solid var(--table-border-color);
@@ -658,14 +720,68 @@ body.product-page {
} }
} }
.card-indicator { .cart-indicator {
margin-left: 6px; position: absolute;
text-align: center;
width: 22px;
height: 22px;
left: calc(100% - 40px);
top: 22px;
border-radius: 66px;
box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
background: white;
color: var(--primary-color);
font-size: 14px;
} }
.like-action { .like-action {
visibility: hidden;
text-align: center; text-align: center;
margin-top: -2px; position: absolute;
margin-left: 12px; cursor: pointer;
width: 28px;
height: 28px;
left: 20px;
top: 20px;
/* White */
background: white;
box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
border-radius: 66px;
&.like-action-wished {
visibility: visible !important;
}
@media (max-width: 992px) {
visibility: visible !important;
}
}
.like-action-list {
visibility: hidden;
text-align: center;
position: absolute;
cursor: pointer;
width: 28px;
height: 28px;
left: 20px;
top: 0;
/* White */
background: white;
box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
border-radius: 66px;
&.like-action-wished {
visibility: visible !important;
}
@media (max-width: 992px) {
visibility: visible !important;
}
} }
.like-animate { .like-animate {
@@ -674,21 +790,19 @@ body.product-page {
@keyframes expand { @keyframes expand {
30% { 30% {
transform: scale(1.6); transform: scale(1.3);
} }
50% { 50% {
transform: scale(0.8); transform: scale(0.8);
} }
70% { 70% {
transform: scale(1.3); transform: scale(1.1);
} }
100% { 100% {
transform: scale(1); transform: scale(1);
} }
} }
@keyframes heart { 0%, 17.5% { font-size: 0; } }
.not-wished { .not-wished {
cursor: pointer; cursor: pointer;
stroke: #F47A7A !important; stroke: #F47A7A !important;
@@ -715,52 +829,52 @@ body.product-page {
} }
.btn-explore-variants { .btn-explore-variants {
visibility: hidden;
box-shadow: none; box-shadow: none;
margin: var(--margin-sm) 0; margin: var(--margin-sm) 0;
width: 90px; width: 90px;
max-height: 50px; // to avoid resizing on window resize max-height: 50px; // to avoid resizing on window resize
flex: none; flex: none;
transition: 0.3s ease; transition: 0.3s ease;
color: var(--orange-500);
background-color: white; color: white;
background-color: var(--orange-500);
border: 1px solid var(--orange-500); border: 1px solid var(--orange-500);
font-size: 13px; font-size: 13px;
&:hover { &:hover {
color: white; color: white;
background-color: var(--orange-500);
} }
} }
.btn-add-to-cart-list{ .btn-add-to-cart-list{
visibility: hidden;
box-shadow: none; box-shadow: none;
margin: var(--margin-sm) 0; margin: var(--margin-sm) 0;
margin-top: auto !important;
max-height: 50px; // to avoid resizing on window resize max-height: 50px; // to avoid resizing on window resize
flex: none; flex: none;
transition: 0.3s ease; transition: 0.3s ease;
}
.not-added {
color: var(--primary-color);
background-color: transparent;
border: 1px solid var(--blue-500);
font-size: 13px;
&:hover {
background-color: var(--primary-color);
color: white;
}
}
.added-to-cart {
background-color: var(--dark-green-400);
color: white;
border: 2px solid var(--green-300);
font-size: 13px; font-size: 13px;
&:hover { &:hover {
color: white; color: white;
} }
@media (max-width: 992px) {
visibility: visible !important;
}
}
.go-to-cart-grid {
max-height: 30px;
margin-top: 1rem !important;
}
.go-to-cart {
max-height: 30px;
float: right;
} }
.wishlist-cart-not-added { .wishlist-cart-not-added {
@@ -872,6 +986,41 @@ body.product-page {
font-size: 14px; font-size: 14px;
} }
#search-results-container {
padding: .25rem 1rem;
.category-chip {
background-color: var(--gray-100);
border: none !important;
box-shadow: none;
}
.recent-search {
padding: .5rem .5rem;
border-radius: var(--border-radius);
&:hover {
background-color: var(--gray-100);
}
}
}
#search-box {
padding-left: 2.5rem;
}
.search-icon {
position: absolute;
left: 0;
top: 0;
width: 2.5rem;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
padding-bottom: 1px;
}
#toggle-view { #toggle-view {
float: right; float: right;
} }

View File

@@ -102,6 +102,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
context.no_breadcrumbs = False context.no_breadcrumbs = False
context.title = self.website_title or self.name context.title = self.website_title or self.name
context.name = self.name context.name = self.name
context.item_group_name = self.item_group_name
return context return context

View File

@@ -2,18 +2,7 @@
{% extends "templates/web.html" %} {% extends "templates/web.html" %}
{% block header %} {% block header %}
<div class="row mb-6" style="width: 65vw"> <div class="mb-6">{{ _(item_group_name) }}</div>
<div class="mb-6 col-4 order-1">{{ title }}</div>
<div class="input-group mb-6 col-8 order-2">
<div class="dropdown w-100" id="dropdownMenuSearch">
<input type="search" name="query" id="search-box" class="form-control font-md"
placeholder="Search for products..."
aria-label="Product" aria-describedby="button-addon2">
<!-- Results dropdown rendered in product_search.js -->
</div>
</div>
</div>
{% endblock header %} {% endblock header %}
{% block script %} {% block script %}

View File

@@ -1,20 +1,9 @@
{% from "erpnext/templates/includes/macros.html" import attribute_filter_section, field_filter_section, discount_range_filters %} {% from "erpnext/templates/includes/macros.html" import attribute_filter_section, field_filter_section, discount_range_filters %}
{% extends "templates/web.html" %} {% extends "templates/web.html" %}
{% block title %}{{ _('Products') }}{% endblock %} {% block title %}{{ _('All Products') }}{% endblock %}
{% block header %} {% block header %}
<div class="row mb-6" style="width: 65vw"> <div class="mb-6">{{ _('All Products') }}</div>
<div class="mb-6 col-4 order-1">{{ _('Products') }}</div>
<div class="input-group mb-6 col-8 order-2">
<div class="dropdown w-100" id="dropdownMenuSearch">
<input type="search" name="query" id="search-box" class="form-control font-md"
placeholder="Search for products..."
aria-label="Product" aria-describedby="button-addon2">
<!-- Results dropdown rendered in product_search.js -->
</div>
</div>
</div>
{% endblock header %} {% endblock header %}
{% block page_content %} {% block page_content %}

View File

@@ -9,8 +9,6 @@ $(() => {
// Render Product Views, Filters & Search // Render Product Views, Filters & Search
frappe.require('/assets/js/e-commerce.min.js', function() { frappe.require('/assets/js/e-commerce.min.js', function() {
new erpnext.ProductSearch();
new erpnext.ProductView({ new erpnext.ProductView({
view_type: view_type, view_type: view_type,
products_section: $('#product-listing'), products_section: $('#product-listing'),