feat: Wishlist from card actions
- Add remove items from wishlist
- Wishlist icon at nav bar
- Animate wishlist icon in card and navbar
- Remember wished state after refresh as well
diff --git a/erpnext/e_commerce/doctype/wishlist/wishlist.py b/erpnext/e_commerce/doctype/wishlist/wishlist.py
index 94e2754..7527c6f 100644
--- a/erpnext/e_commerce/doctype/wishlist/wishlist.py
+++ b/erpnext/e_commerce/doctype/wishlist/wishlist.py
@@ -3,8 +3,52 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-# import frappe
+import frappe
from frappe.model.document import Document
class Wishlist(Document):
pass
+
+@frappe.whitelist()
+def add_to_wishlist(item_code, price):
+ """Insert Item into wishlist."""
+ web_item_data = frappe.db.get_value("Website Item", {"item_code": item_code},
+ ["image", "website_warehouse", "name", "item_name"], as_dict=1)
+
+ wished_item_dict = {
+ "item_code": item_code,
+ "item_name": web_item_data.get("item_name"),
+ "website_item": web_item_data.get("name"),
+ "price": frappe.utils.flt(price),
+ "image": web_item_data.get("image"),
+ "website_warehouse": web_item_data.get("website_warehouse")
+ }
+
+ if not frappe.db.exists("Wishlist", frappe.session.user):
+ # initialise wishlist
+ wishlist = frappe.get_doc({"doctype": "Wishlist"})
+ wishlist.user = frappe.session.user
+ wishlist.append("items", wished_item_dict)
+ wishlist.save(ignore_permissions=True)
+ else:
+ wishlist = frappe.get_doc("Wishlist", frappe.session.user)
+ item = wishlist.append('items', wished_item_dict)
+ item.db_insert()
+
+ if hasattr(frappe.local, "cookie_manager"):
+ frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items)))
+
+@frappe.whitelist()
+def remove_from_wishlist(item_code):
+ if frappe.db.exists("Wishlist Items", {"item_code": item_code}):
+ frappe.db.sql("""
+ delete
+ from `tabWishlist Items`
+ where item_code=%(item_code)s
+ """%{"item_code": frappe.db.escape(item_code)})
+
+ frappe.db.commit()
+
+ wishlist = frappe.get_doc("Wishlist", frappe.session.user)
+ if hasattr(frappe.local, "cookie_manager"):
+ frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items)))
\ No newline at end of file
diff --git a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json
index 29f4066..18065a8 100644
--- a/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json
+++ b/erpnext/e_commerce/doctype/wishlist_items/wishlist_items.json
@@ -12,6 +12,8 @@
"item_details_section",
"description",
"column_break_7",
+ "section_break_8",
+ "price",
"image",
"image_view",
"warehouse_section",
@@ -52,6 +54,7 @@
},
{
"fetch_from": "item_code.description",
+ "fetch_if_empty": 1,
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description"
@@ -62,6 +65,7 @@
},
{
"fetch_from": "item_code.image",
+ "fetch_if_empty": 1,
"fieldname": "image",
"fieldtype": "Attach",
"hidden": 1,
@@ -69,6 +73,7 @@
},
{
"fetch_from": "item_code.image",
+ "fetch_if_empty": 1,
"fieldname": "image_view",
"fieldtype": "Image",
"hidden": 1,
@@ -87,12 +92,21 @@
"in_list_view": 1,
"label": "Warehouse",
"options": "Warehouse"
+ },
+ {
+ "fieldname": "section_break_8",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "price",
+ "fieldtype": "Float",
+ "label": "Price"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-03-10 19:13:41.310816",
+ "modified": "2021-03-12 18:23:03.487891",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Wishlist Items",
diff --git a/erpnext/e_commerce/product_query.py b/erpnext/e_commerce/product_query.py
index 9743b76..28d33e6 100644
--- a/erpnext/e_commerce/product_query.py
+++ b/erpnext/e_commerce/product_query.py
@@ -67,6 +67,7 @@
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
if product_info:
item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
+ item.price = product_info['price'].get('price_list_rate')
if self.settings.show_stock_availability and item.get("website_warehouse"):
stock_qty = frappe.utils.flt(
@@ -78,6 +79,11 @@
"actual_qty")
)
item.in_stock = "green" if stock_qty else "red"
+
+ item.wished = False
+ if frappe.db.exists("Wishlist Items", {"item_code": item.item_code}):
+ item.wished = True
+
return result
def query_items(self, conditions, or_conditions, substitutions, start=0):
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
index c2c94a3..011f29c 100644
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -138,7 +138,7 @@
"additional_notes": additional_notes
})
else:
- quotation_items[0].qty = qty + 1
+ quotation_items[0].qty = qty
quotation_items[0].additional_notes = additional_notes
apply_cart_settings(quotation=quotation)
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 6b70dab..a891121 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -11,7 +11,8 @@
],
"js/erpnext-web.min.js": [
"public/js/website_utils.js",
- "public/js/shopping_cart.js"
+ "public/js/shopping_cart.js",
+ "public/js/wishlist.js"
],
"css/erpnext-web.css": [
"public/scss/website.scss",
diff --git a/erpnext/public/js/wishlist.js b/erpnext/public/js/wishlist.js
new file mode 100644
index 0000000..328bdb9
--- /dev/null
+++ b/erpnext/public/js/wishlist.js
@@ -0,0 +1,39 @@
+frappe.provide("erpnext.e_commerce");
+var wishlist = erpnext.e_commerce;
+
+frappe.ready(function() {
+ $(".wishlist").toggleClass('hidden', true);
+ wishlist.set_wishlist_count();
+});
+
+$.extend(wishlist, {
+ set_wishlist_count: function() {
+ var wish_count = frappe.get_cookie("wish_count");
+ if(frappe.session.user==="Guest") {
+ wish_count = 0;
+ }
+
+ if(wish_count) {
+ $(".wishlist").toggleClass('hidden', false);
+ }
+
+ var $wishlist = $('.wishlist-icon');
+ var $badge = $wishlist.find("#wish-count");
+
+ if(parseInt(wish_count) === 0 || wish_count === undefined) {
+ $wishlist.css("display", "none");
+ }
+ else {
+ $wishlist.css("display", "inline");
+ }
+ if(wish_count) {
+ $badge.html(wish_count);
+ $wishlist.addClass('cart-animate');
+ setTimeout(() => {
+ $wishlist.removeClass('cart-animate');
+ }, 500);
+ } else {
+ $badge.remove();
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 8380f6c..3d66f14 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -349,20 +349,20 @@
}
}
-.cart-icon {
- .cart-badge {
- position: relative;
- top: -10px;
- left: -12px;
- background: var(--red-600);
- width: 16px;
- align-items: center;
- height: 16px;
- font-size: 10px;
- border-radius: 50%;
- }
+
+.shopping-badge {
+ position: relative;
+ top: -10px;
+ left: -12px;
+ background: var(--red-600);
+ width: 16px;
+ align-items: center;
+ height: 16px;
+ font-size: 10px;
+ border-radius: 50%;
}
+
.cart-animate {
animation: wiggle 0.5s linear;
}
@@ -555,7 +555,28 @@
margin-left: 12px;
}
-.wish-icon {
+.like-animate {
+ animation: expand cubic-bezier(0.04, 0.4, 0.5, 0.95) 1.6s forwards 1;
+}
+
+@keyframes expand {
+ 30% {
+ transform: scale(1.6);
+ }
+ 50% {
+ transform: scale(0.8);
+ }
+ 70% {
+ transform: scale(1.3);
+ }
+ 100% {
+ transform: scale(1);
+ }
+ }
+
+@keyframes heart { 0%, 17.5% { font-size: 0; } }
+
+.not-wished {
cursor: pointer;
stroke: #F47A7A !important;
@@ -565,10 +586,8 @@
}
.wished {
- .wish-icon {
- stroke: none;
- fill: #F47A7A !important;
- }
+ stroke: none;
+ fill: #F47A7A !important;
}
.list-row-checkbox {
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index 818316c..743daaf 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -127,12 +127,11 @@
<span class="indicator {{ item.in_stock }} card-indicator"></span>
{% endif %}
{% if not item.has_variants %}
- <input class="level-item list-row-checkbox hidden-xs"
- type="checkbox" data-name="{{ title }}" style="display: none !important;">
<div class="like-action"
- data-name="{{ title }}" data-doctype="Item">
+ data-item-code="{{ item.item_code }}" data-price="{{ item.price }}">
<svg class="icon sm">
- <use class="wish-icon" href="#icon-heart"></use>
+ {%- set icon_class = "wished" if item.wished else "not-wished"-%}
+ <use class="{{ icon_class }} wish-icon" href="#icon-heart"></use>
</svg>
</div>
{% endif %}
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html
index 2912206..54ed98a 100644
--- a/erpnext/templates/includes/navbar/navbar_items.html
+++ b/erpnext/templates/includes/navbar/navbar_items.html
@@ -6,7 +6,15 @@
<svg class="icon icon-lg">
<use href="#icon-assets"></use>
</svg>
- <span class="badge badge-primary cart-badge" id="cart-count"></span>
+ <span class="badge badge-primary shopping-badge" id="cart-count"></span>
+ </a>
+ </li>
+ <li class="wishlist wishlist-icon hidden">
+ <a class="nav-link" href="/cart">
+ <svg class="icon icon-lg">
+ <use href="#icon-heart"></use>
+ </svg>
+ <span class="badge badge-primary shopping-badge" id="wish-count"></span>
</a>
</li>
{% endblock %}
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index 4572ee7..fc1a3f4 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -73,6 +73,11 @@
}
bind_card_actions() {
+ this.bind_add_to_cart_action();
+ this.bind_wishlist_action();
+ }
+
+ bind_add_to_cart_action() {
$('.page_content').on('click', '.btn-add-to-cart-list', (e) => {
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
@@ -91,18 +96,77 @@
animate_add_to_cart(button) {
// Create 'added to cart' animation
let btn_id = "#" + button[0].id;
- button.removeClass('not-added');
- button.addClass('added-to-cart');
+ this.toggle_button_class(button, 'not-added', 'added-to-cart');
$(btn_id).text('Added to Cart');
// undo
setTimeout(() => {
- button.removeClass('added-to-cart');
- button.addClass('not-added');
+ this.toggle_button_class(button, 'added-to-cart', 'not-added');
$(btn_id).text('Add to Cart');
}, 2000);
}
+ bind_wishlist_action() {
+ $('.page_content').on('click', '.like-action', (e) => {
+ const $btn = $(e.currentTarget);
+ const $wish_icon = $btn.find('.wish-icon');
+ let me = this;
+
+ if ($wish_icon.hasClass('wished')) {
+ // un-wish item
+ $btn.removeClass("like-animate");
+ this.toggle_button_class($wish_icon, 'wished', 'not-wished');
+ frappe.call({
+ type: "POST",
+ method: "erpnext.e_commerce.doctype.wishlist.wishlist.remove_from_wishlist",
+ args: {
+ item_code: $btn.data('item-code')
+ },
+ callback: function (r) {
+ if (r.exc) {
+ me.toggle_button_class($wish_icon, 'wished', 'not-wished');
+ frappe.msgprint({
+ message: __("Sorry, something went wrong. Please refresh."),
+ indicator: "red",
+ title: __("Note")}
+ );
+ } else {
+ erpnext.e_commerce.set_wishlist_count();
+ }
+ }
+ });
+ } else {
+ $btn.addClass("like-animate");
+ this.toggle_button_class($wish_icon, 'not-wished', 'wished');
+ frappe.call({
+ type: "POST",
+ method: "erpnext.e_commerce.doctype.wishlist.wishlist.add_to_wishlist",
+ args: {
+ item_code: $btn.data('item-code'),
+ price: $btn.data('price')
+ },
+ callback: function (r) {
+ if (r.exc) {
+ me.toggle_button_class($wish_icon, 'wished', 'not-wished');
+ frappe.msgprint({
+ message: __("Sorry, something went wrong. Please refresh."),
+ indicator: "red",
+ title: __("Note")}
+ );
+ } else {
+ erpnext.e_commerce.set_wishlist_count();
+ }
+ }
+ });
+ }
+ });
+ }
+
+ toggle_button_class(button, remove, add) {
+ button.removeClass(remove);
+ button.addClass(add);
+ }
+
bind_search() {
$('input[type=search]').on('keydown', (e) => {
if (e.keyCode === 13) {