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:
parent
c0811c4c74
commit
b5e7f04b33
@ -61,7 +61,7 @@ def add_item_review(web_item, title, rating, comment=None):
|
||||
doc.published_on = datetime.today().strftime("%d %B %Y")
|
||||
doc.insert()
|
||||
|
||||
def get_customer():
|
||||
def get_customer(silent=False):
|
||||
user = frappe.session.user
|
||||
contact_name = get_contact_name(user)
|
||||
customer = None
|
||||
@ -75,5 +75,7 @@ def get_customer():
|
||||
|
||||
if customer:
|
||||
return frappe.db.get_value("Customer", customer)
|
||||
elif silent:
|
||||
return None
|
||||
else:
|
||||
frappe.throw(_("You are not verified to write a review yet. Please contact us for verification."))
|
@ -310,7 +310,7 @@
|
||||
{
|
||||
"description": "Short Description for List View",
|
||||
"fieldname": "short_description",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Short Website Description"
|
||||
}
|
||||
],
|
||||
@ -318,7 +318,7 @@
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-08 19:25:15.115746",
|
||||
"modified": "2021-07-11 20:49:45.415421",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Website Item",
|
||||
|
@ -22,7 +22,7 @@ erpnext.ProductGrid = class {
|
||||
|
||||
this.items.forEach(item => {
|
||||
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 += me.get_image_html(item, title);
|
||||
@ -60,13 +60,20 @@ erpnext.ProductGrid = class {
|
||||
|
||||
get_card_body_html(item, title, settings) {
|
||||
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;">
|
||||
`;
|
||||
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
|
||||
@ -76,29 +83,28 @@ erpnext.ProductGrid = class {
|
||||
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 += `</div>`; // close div on line 49
|
||||
|
||||
return body_html;
|
||||
}
|
||||
|
||||
get_title_with_indicator(item, title, settings) {
|
||||
get_title(item, title) {
|
||||
let title_html = `
|
||||
<a href="/${ item.route || '#' }">
|
||||
<div class="product-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;
|
||||
}
|
||||
|
||||
get_wishlist_icon(item) {
|
||||
let icon_class = item.wished ? "wished" : "not-wished";
|
||||
return `
|
||||
<div class="like-action"
|
||||
<div class="like-action ${ item.wished ? "like-action-wished" : ''}"
|
||||
data-item-code="${ item.item_code }"
|
||||
data-price="${ item.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) {
|
||||
let price_html = `
|
||||
<div class="product-price">
|
||||
@ -117,10 +131,10 @@ erpnext.ProductGrid = class {
|
||||
|
||||
if (item.formatted_mrp) {
|
||||
price_html += `
|
||||
<small class="ml-1 text-muted">
|
||||
<s>${ item.formatted_mrp }</s>
|
||||
<small class="striked-price">
|
||||
<s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" }</s>
|
||||
</small>
|
||||
<small class="ml-1" style="color: #F47A7A; font-weight: 500;">
|
||||
<small class="ml-1 product-info-green">
|
||||
${ item.discount } OFF
|
||||
</small>
|
||||
`;
|
||||
@ -129,6 +143,13 @@ erpnext.ProductGrid = class {
|
||||
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) {
|
||||
if (item.has_variants) {
|
||||
return `
|
||||
@ -138,13 +159,29 @@ erpnext.ProductGrid = class {
|
||||
</div>
|
||||
</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 `
|
||||
<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 }">
|
||||
<span class="mr-2">
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-assets"></use>
|
||||
</svg>
|
||||
</span>
|
||||
${ __('Add to Cart') }
|
||||
</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 {
|
||||
return ``;
|
||||
|
@ -24,8 +24,8 @@ erpnext.ProductList = class {
|
||||
let title = item.web_item_name || item.item_name || item.item_code || "";
|
||||
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 += me.get_image_html(item, title);
|
||||
html += `<div class='row list-row w-100'>`;
|
||||
html += me.get_image_html(item, title, me.settings);
|
||||
html += me.get_row_body_html(item, title, me.settings);
|
||||
html += `</div>`;
|
||||
});
|
||||
@ -34,20 +34,23 @@ erpnext.ProductList = class {
|
||||
$product_wrapper.append(html);
|
||||
}
|
||||
|
||||
get_image_html(item, title) {
|
||||
get_image_html(item, title, settings) {
|
||||
let image = item.website_image || item.image;
|
||||
let wishlist_enabled = !item.has_variants && settings.enable_wishlist;
|
||||
let image_html = ``;
|
||||
|
||||
if (image) {
|
||||
return `
|
||||
image_html += `
|
||||
<div class="col-2 border text-center rounded list-image">
|
||||
<a class="product-link product-list-link" href="/${ item.route || '#' }">
|
||||
<img itemprop="image" class="website-image h-100 w-100" alt="${ title }"
|
||||
src="${ image }">
|
||||
</a>
|
||||
${ wishlist_enabled ? this.get_wishlist_icon(item): '' }
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
return `
|
||||
image_html += `
|
||||
<div class="col-2 border text-center rounded list-image">
|
||||
<a class="product-link product-list-link" href="/${ item.route || '#' }"
|
||||
style="text-decoration: none">
|
||||
@ -55,13 +58,16 @@ erpnext.ProductList = class {
|
||||
${ frappe.get_abbr(title) }
|
||||
</div>
|
||||
</a>
|
||||
${ wishlist_enabled ? this.get_wishlist_icon(item): '' }
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return image_html;
|
||||
}
|
||||
|
||||
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_item_details(item, settings);
|
||||
body_html += `</div>`;
|
||||
@ -76,18 +82,11 @@ erpnext.ProductList = class {
|
||||
style="color: var(--gray-800); font-weight: 500;">
|
||||
${ title }
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (item.in_stock && settings.show_stock_availability) {
|
||||
title_html += `<span class="indicator ${ item.in_stock } card-indicator"></span>`;
|
||||
}
|
||||
title_html += `</div>`;
|
||||
|
||||
if (settings.enable_wishlist || settings.enabled) {
|
||||
if (settings.enabled) {
|
||||
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 += `</div>`;
|
||||
}
|
||||
@ -96,12 +95,12 @@ erpnext.ProductList = class {
|
||||
return title_html;
|
||||
}
|
||||
|
||||
get_item_details(item) {
|
||||
get_item_details(item, settings) {
|
||||
let details = `
|
||||
<p class="product-code">
|
||||
Item Code : ${ item.item_code }
|
||||
${ item.item_group } | Item Code : ${ item.item_code }
|
||||
</p>
|
||||
<div class="text-muted mt-2">
|
||||
<div class="mt-2" style="color: var(--gray-600) !important; font-size: 13px;">
|
||||
${ item.short_description || '' }
|
||||
</div>
|
||||
<div class="product-price">
|
||||
@ -110,24 +109,33 @@ erpnext.ProductList = class {
|
||||
|
||||
if (item.formatted_mrp) {
|
||||
details += `
|
||||
<small class="ml-1 text-muted">
|
||||
<s>${ item.formatted_mrp }</s>
|
||||
<small class="striked-price">
|
||||
<s>${ item.formatted_mrp ? item.formatted_mrp.replace(/ +/g, "") : "" }</s>
|
||||
</small>
|
||||
<small class="ml-1" style="color: #F47A7A; font-weight: 500;">
|
||||
<small class="ml-1 product-info-green">
|
||||
${ item.discount } OFF
|
||||
</small>
|
||||
`;
|
||||
}
|
||||
|
||||
details += this.get_stock_availability(item, settings);
|
||||
details += `</div>`;
|
||||
|
||||
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) {
|
||||
let icon_class = item.wished ? "wished" : "not-wished";
|
||||
|
||||
return `
|
||||
<div class="like-action mr-4"
|
||||
<div class="like-action-list ${ item.wished ? "like-action-wished" : ''}"
|
||||
data-item-code="${ item.item_code }"
|
||||
data-price="${ item.price || '' }"
|
||||
data-formatted-price="${ item.formatted_price || '' }">
|
||||
@ -142,19 +150,43 @@ erpnext.ProductList = class {
|
||||
if (item.has_variants) {
|
||||
return `
|
||||
<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') }
|
||||
</div>
|
||||
</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 `
|
||||
<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 }"
|
||||
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') }
|
||||
</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 {
|
||||
return ``;
|
||||
|
@ -2,7 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
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 erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
||||
@ -41,7 +42,7 @@ class ProductQuery:
|
||||
"""
|
||||
# track if discounts included in field filters
|
||||
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)
|
||||
|
||||
@ -62,7 +63,11 @@ class ProductQuery:
|
||||
|
||||
# sort combined results by ranking
|
||||
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 = []
|
||||
if discount_list:
|
||||
@ -193,7 +198,7 @@ class ProductQuery:
|
||||
)
|
||||
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."""
|
||||
for item in result:
|
||||
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
|
||||
@ -205,6 +210,8 @@ class ProductQuery:
|
||||
if self.settings.show_stock_availability:
|
||||
self.get_stock_availability(item)
|
||||
|
||||
item.in_cart = item.item_code in cart_items
|
||||
|
||||
item.wished = False
|
||||
if frappe.db.exists("Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}):
|
||||
item.wished = True
|
||||
@ -227,13 +234,31 @@ class ProductQuery:
|
||||
|
||||
def get_stock_availability(self, item):
|
||||
"""Modify item object and add stock details."""
|
||||
item.in_stock = False
|
||||
|
||||
if item.get("website_warehouse"):
|
||||
stock_qty = frappe.utils.flt(
|
||||
frappe.db.get_value("Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")},
|
||||
"actual_qty"))
|
||||
item.in_stock = "green" if stock_qty else "red"
|
||||
elif not frappe.db.get_value("Item", item.item_code, "is_stock_item"):
|
||||
item.in_stock = "green" # non-stock item will always be available
|
||||
item.in_stock = bool(stock_qty)
|
||||
|
||||
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):
|
||||
"""Combine results with context of website item groups into item results."""
|
||||
|
@ -10,8 +10,6 @@ erpnext.ProductSearch = class {
|
||||
setupSearchDropDown() {
|
||||
this.search_area = $("#dropdownMenuSearch");
|
||||
this.setupSearchResultContainer();
|
||||
this.setupProductsContainer();
|
||||
this.setupCategoryRecentsContainer();
|
||||
this.populateRecentSearches();
|
||||
}
|
||||
|
||||
@ -82,60 +80,46 @@ erpnext.ProductSearch = class {
|
||||
<div class="overflow-hidden shadow dropdown-menu w-100 hidden"
|
||||
id="search-results-container"
|
||||
aria-labelledby="dropdownMenuSearch"
|
||||
style="display: flex;">
|
||||
style="display: flex; flex-direction: column;">
|
||||
</div>
|
||||
`).find("#search-results-container");
|
||||
|
||||
this.setupCategoryContainer()
|
||||
this.setupProductsContainer();
|
||||
this.setupRecentsContainer();
|
||||
}
|
||||
|
||||
setupProductsContainer() {
|
||||
let $products_section = this.search_dropdown.append(`
|
||||
<div class="col-7 mr-2 mt-1"
|
||||
id="product-results"
|
||||
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 ...") }
|
||||
this.products_container = this.search_dropdown.append(`
|
||||
<div id="product-results mt-2">
|
||||
<div id="product-scroll" style="overflow: scroll; max-height: 300px">
|
||||
</div>
|
||||
</div>
|
||||
`).find("#product-scroll");
|
||||
}
|
||||
|
||||
setupCategoryRecentsContainer() {
|
||||
let $category_recents_section = $("#search-results-container").append(`
|
||||
<div id="category-recents-container"
|
||||
class="col-5 mt-2 h-100"
|
||||
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>
|
||||
setupCategoryContainer() {
|
||||
this.category_container = this.search_dropdown.append(`
|
||||
<div class="category-container mt-2 mb-1">
|
||||
<div class="category-chips">
|
||||
</div>
|
||||
</div>
|
||||
`).find(".categories");
|
||||
`).find(".category-chips");
|
||||
}
|
||||
|
||||
let $recents_section = $("#category-recents-container").append(`
|
||||
<div class="mb-2 mt-4 recent-searches">
|
||||
<div style="border-bottom: 1px solid var(--gray-200);">
|
||||
${ __("Recent") }
|
||||
setupRecentsContainer() {
|
||||
let $recents_section = this.search_dropdown.append(`
|
||||
<div class="mb-2 mt-2 recent-searches">
|
||||
<div>
|
||||
<b>${ __("Recent") }</b>
|
||||
</div>
|
||||
</div>
|
||||
`).find(".recent-searches");
|
||||
|
||||
this.recents_container = $recents_section.append(`
|
||||
<div id="recent-chips" style="padding: 1rem 0;">
|
||||
<div id="recents" style="padding: .25rem 0 1rem 0;">
|
||||
</div>
|
||||
`).find("#recent-chips");
|
||||
`).find("#recents");
|
||||
}
|
||||
|
||||
getRecentSearches() {
|
||||
@ -144,12 +128,12 @@ erpnext.ProductSearch = class {
|
||||
|
||||
attachEventListenersToChips() {
|
||||
let me = this;
|
||||
const chips = $(".recent-chip");
|
||||
const chips = $(".recent-search");
|
||||
window.chips = chips;
|
||||
|
||||
for (let chip of chips) {
|
||||
chip.addEventListener("click", () => {
|
||||
me.searchBox[0].value = chip.innerText;
|
||||
me.searchBox[0].value = chip.innerText.trim();
|
||||
|
||||
// Start search with `recent query`
|
||||
me.searchBox.trigger("input");
|
||||
@ -179,15 +163,22 @@ erpnext.ProductSearch = class {
|
||||
let recents = this.getRecentSearches();
|
||||
|
||||
if (!recents.length) {
|
||||
this.recents_container.html(`<span class=""text-muted">No searches yet.</span>`);
|
||||
return;
|
||||
}
|
||||
|
||||
let html = "";
|
||||
recents.forEach((key) => {
|
||||
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 }
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
@ -197,11 +188,7 @@ erpnext.ProductSearch = class {
|
||||
|
||||
populateResults(data) {
|
||||
if (data.length === 0 || data.message.results.length === 0) {
|
||||
let empty_html = `
|
||||
<div class="mt-6 w-100 text-muted" style="font-weight: 400; text-align: center;">
|
||||
${ __('No results') }
|
||||
</div>
|
||||
`;
|
||||
let empty_html = ``;
|
||||
this.products_container.html(empty_html);
|
||||
return;
|
||||
}
|
||||
@ -228,21 +215,27 @@ erpnext.ProductSearch = class {
|
||||
populateCategoriesList(data) {
|
||||
if (data.length === 0 || data.message.results.length === 0) {
|
||||
let empty_html = `
|
||||
<span class="text-muted" style="font-weight: 400;">
|
||||
${__('No results')}
|
||||
</span>
|
||||
<div class="category-container mt-2">
|
||||
<div class="category-chips">
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
this.category_container.html(empty_html);
|
||||
return;
|
||||
}
|
||||
|
||||
let html = "";
|
||||
let html = `
|
||||
<div class="mb-2">
|
||||
<b>${ __("Categories") }</b>
|
||||
</div>
|
||||
`;
|
||||
let search_results = data.message.results;
|
||||
search_results.forEach((category) => {
|
||||
html += `
|
||||
<div class="mb-2" style="font-weight: 400;">
|
||||
<a href="/${category.route}">${category.name}</a>
|
||||
</div>
|
||||
<a href="/${category.route}" class="btn btn-sm category-chip mr-2 mb-2"
|
||||
style="font-size: 13px" role="button">
|
||||
${ category.name }
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
|
||||
|
@ -12,11 +12,25 @@ erpnext.ProductView = class {
|
||||
|
||||
make(from_filters=false) {
|
||||
this.products_section.empty();
|
||||
this.prepare_view_toggler();
|
||||
this.prepare_toolbar();
|
||||
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() {
|
||||
|
||||
if (!$("#list").length || !$("#image-view").length) {
|
||||
this.render_view_toggler();
|
||||
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() {
|
||||
$(".toolbar").append(`<div class="toggle-container col-6"></div>`);
|
||||
|
||||
["btn-list-view", "btn-grid-view"].forEach(view => {
|
||||
let icon = view === "btn-list-view" ? "list" : "image-view";
|
||||
this.products_section.append(`
|
||||
<div class="form-group mb-0" id="toggle-view">
|
||||
<button id="${ icon }" class="btn ${ view } mr-2">
|
||||
<span>
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-${ icon }"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>`);
|
||||
$(".toggle-container").append(`
|
||||
<div class="form-group mb-0" id="toggle-view">
|
||||
<button id="${ icon }" class="btn ${ view } mr-2">
|
||||
<span>
|
||||
<svg class="icon icon-md">
|
||||
<use href="#icon-${ icon }"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
}
|
||||
|
||||
@ -448,9 +488,6 @@ erpnext.ProductView = class {
|
||||
render_item_sub_categories(categories) {
|
||||
if (categories && categories.length) {
|
||||
let sub_group_html = `
|
||||
<div class="sub-category-container">
|
||||
<div class="heading"> ${ __('Sub Categories') } </div>
|
||||
</div>
|
||||
<div class="sub-category-container scroll-categories">
|
||||
`;
|
||||
|
||||
|
@ -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) {
|
||||
button.removeClass(remove);
|
||||
button.addClass(add);
|
||||
@ -189,7 +176,10 @@ $.extend(shopping_cart, {
|
||||
const $btn = $(e.currentTarget);
|
||||
$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');
|
||||
e_commerce.shopping_cart.update_cart({
|
||||
|
@ -79,7 +79,7 @@ $.extend(wishlist, {
|
||||
|
||||
bind_wishlist_action() {
|
||||
// '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 $wish_icon = $btn.find('.wish-icon');
|
||||
let me = this;
|
||||
@ -101,6 +101,7 @@ $.extend(wishlist, {
|
||||
if ($wish_icon.hasClass('wished')) {
|
||||
// un-wish item
|
||||
$btn.removeClass("like-animate");
|
||||
$btn.addClass("like-action-wished");
|
||||
this.toggle_button_class($wish_icon, 'wished', 'not-wished');
|
||||
|
||||
let args = { item_code: $btn.data('item-code') };
|
||||
@ -111,6 +112,7 @@ $.extend(wishlist, {
|
||||
} else {
|
||||
// wish item
|
||||
$btn.addClass("like-animate");
|
||||
$btn.addClass("like-action-wished");
|
||||
this.toggle_button_class($wish_icon, 'not-wished', 'wished');
|
||||
|
||||
let args = {
|
||||
|
@ -1,5 +1,9 @@
|
||||
@import "frappe/public/scss/common/mixins";
|
||||
|
||||
:root {
|
||||
--green-info: #38A160;
|
||||
}
|
||||
|
||||
body.product-page {
|
||||
background: var(--gray-50);
|
||||
}
|
||||
@ -80,6 +84,7 @@ body.product-page {
|
||||
|
||||
.item-card-group-section {
|
||||
.card {
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@ -89,6 +94,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 {
|
||||
height: 210px;
|
||||
width: 100%;
|
||||
@ -102,12 +120,10 @@ body.product-page {
|
||||
|
||||
.no-image {
|
||||
@include flex(flex, center, center, null);
|
||||
height: 200px;
|
||||
margin: 0 auto;
|
||||
margin-top: var(--margin-xl);
|
||||
height: 220px;
|
||||
background: var(--gray-100);
|
||||
width: 80%;
|
||||
border-radius: var(--border-radius);
|
||||
width: 100%;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
font-size: 2rem;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
@ -123,6 +139,11 @@ body.product-page {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-body-flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-title {
|
||||
font-size: 14px;
|
||||
color: var(--gray-800);
|
||||
@ -153,6 +174,24 @@ body.product-page {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
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 {
|
||||
@ -166,6 +205,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"],
|
||||
#page-all-products {
|
||||
.page-header {
|
||||
@ -220,6 +281,7 @@ body.product-page {
|
||||
}
|
||||
|
||||
.list-image {
|
||||
border: none !important;
|
||||
overflow: hidden;
|
||||
max-height: 200px;
|
||||
background-color: white;
|
||||
@ -341,7 +403,7 @@ body.product-page {
|
||||
|
||||
.product-code {
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.item-configurator-dialog {
|
||||
@ -391,7 +453,7 @@ body.product-page {
|
||||
}
|
||||
|
||||
.sub-category-container {
|
||||
padding-bottom: 1rem;
|
||||
padding-bottom: .5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
border-bottom: 1px solid var(--table-border-color);
|
||||
|
||||
@ -668,14 +730,68 @@ body.product-page {
|
||||
}
|
||||
}
|
||||
|
||||
.card-indicator {
|
||||
margin-left: 6px;
|
||||
.cart-indicator {
|
||||
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 {
|
||||
visibility: hidden;
|
||||
text-align: center;
|
||||
margin-top: -2px;
|
||||
margin-left: 12px;
|
||||
position: absolute;
|
||||
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 {
|
||||
@ -684,21 +800,19 @@ body.product-page {
|
||||
|
||||
@keyframes expand {
|
||||
30% {
|
||||
transform: scale(1.6);
|
||||
transform: scale(1.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
70% {
|
||||
transform: scale(1.3);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heart { 0%, 17.5% { font-size: 0; } }
|
||||
|
||||
.not-wished {
|
||||
cursor: pointer;
|
||||
stroke: #F47A7A !important;
|
||||
@ -725,52 +839,52 @@ body.product-page {
|
||||
}
|
||||
|
||||
.btn-explore-variants {
|
||||
visibility: hidden;
|
||||
box-shadow: none;
|
||||
margin: var(--margin-sm) 0;
|
||||
width: 90px;
|
||||
max-height: 50px; // to avoid resizing on window resize
|
||||
flex: none;
|
||||
transition: 0.3s ease;
|
||||
color: var(--orange-500);
|
||||
background-color: white;
|
||||
|
||||
color: white;
|
||||
background-color: var(--orange-500);
|
||||
border: 1px solid var(--orange-500);
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: var(--orange-500);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-to-cart-list{
|
||||
visibility: hidden;
|
||||
box-shadow: none;
|
||||
margin: var(--margin-sm) 0;
|
||||
margin-top: auto !important;
|
||||
max-height: 50px; // to avoid resizing on window resize
|
||||
flex: none;
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
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 {
|
||||
@ -882,6 +996,41 @@ body.product-page {
|
||||
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 {
|
||||
float: right;
|
||||
}
|
||||
|
@ -101,6 +101,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
context.no_breadcrumbs = False
|
||||
context.title = self.website_title or self.name
|
||||
context.name = self.name
|
||||
context.item_group_name = self.item_group_name
|
||||
|
||||
return context
|
||||
|
||||
|
@ -2,18 +2,7 @@
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block header %}
|
||||
<div class="row mb-6" style="width: 65vw">
|
||||
<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>
|
||||
<div class="mb-6">{{ _(item_group_name) }}</div>
|
||||
{% endblock header %}
|
||||
|
||||
{% block script %}
|
||||
|
@ -1,20 +1,9 @@
|
||||
{% from "erpnext/templates/includes/macros.html" import attribute_filter_section, field_filter_section, discount_range_filters %}
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block title %}{{ _('Products') }}{% endblock %}
|
||||
{% block title %}{{ _('All Products') }}{% endblock %}
|
||||
{% block header %}
|
||||
<div class="row mb-6" style="width: 65vw">
|
||||
<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>
|
||||
<div class="mb-6">{{ _('All Products') }}</div>
|
||||
{% endblock header %}
|
||||
|
||||
{% block page_content %}
|
||||
|
@ -9,8 +9,6 @@ $(() => {
|
||||
|
||||
// Render Product Views, Filters & Search
|
||||
frappe.require('/assets/js/e-commerce.min.js', function() {
|
||||
new erpnext.ProductSearch();
|
||||
|
||||
new erpnext.ProductView({
|
||||
view_type: view_type,
|
||||
products_section: $('#product-listing'),
|
||||
|
Loading…
x
Reference in New Issue
Block a user