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 c0811c4c74
commit b5e7f04b33
14 changed files with 440 additions and 196 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.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."))

View File

@ -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",

View File

@ -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 ``;

View File

@ -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 ``;

View File

@ -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."""

View File

@ -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>
`;
});

View File

@ -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">
`;

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) {
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({

View File

@ -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 = {

View File

@ -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;
}

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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'),