feat: Wishlist Page

- Navbar icon with badge count for wishlist
- Wishlist page with cards
- Cards can be moved to cart or removed in a click
- Separated all wishlist related methods into wishlist.js
- Made a common js method(util) to add/remove wishlist items
- Bug fix: Make sure items are removed from session user's wishlist
This commit is contained in:
marination 2021-03-16 00:05:53 +05:30
parent 96cc5068b2
commit 59514408b9
11 changed files with 345 additions and 108 deletions

View File

@ -10,18 +10,22 @@ class Wishlist(Document):
pass
@frappe.whitelist()
def add_to_wishlist(item_code, price):
def add_to_wishlist(item_code, price, formatted_price=None):
"""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)
["image", "website_warehouse", "name", "item_name", "item_group", "route"]
, as_dict=1)
wished_item_dict = {
"item_code": item_code,
"item_name": web_item_data.get("item_name"),
"item_group": web_item_data.get("item_group"),
"website_item": web_item_data.get("name"),
"price": frappe.utils.flt(price),
"formatted_price": formatted_price,
"image": web_item_data.get("image"),
"website_warehouse": web_item_data.get("website_warehouse")
"warehouse": web_item_data.get("website_warehouse"),
"route": web_item_data.get("route")
}
if not frappe.db.exists("Wishlist", frappe.session.user):

View File

@ -9,13 +9,16 @@
"website_item",
"column_break_3",
"item_name",
"item_group",
"item_details_section",
"description",
"column_break_7",
"section_break_8",
"price",
"route",
"image",
"image_view",
"section_break_8",
"price",
"formatted_price",
"warehouse_section",
"warehouse"
],
@ -101,12 +104,28 @@
"fieldname": "price",
"fieldtype": "Float",
"label": "Price"
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
"options": "Item Group"
},
{
"fieldname": "route",
"fieldtype": "Small Text",
"label": "Route"
},
{
"fieldname": "formatted_price",
"fieldtype": "Data",
"label": "Formatted Price"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-03-12 18:23:03.487891",
"modified": "2021-03-15 16:37:40.405333",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Wishlist Items",

View File

@ -81,7 +81,7 @@ class ProductQuery:
item.in_stock = "green" if stock_qty else "red"
item.wished = False
if frappe.db.exists("Wishlist Items", {"item_code": item.item_code}):
if frappe.db.exists("Wishlist Items", {"item_code": item.item_code, "parent": frappe.session.user}):
item.wished = True
return result

View File

@ -186,5 +186,40 @@ $.extend(shopping_cart, {
$(".shopping-cart").toggleClass('hidden', r.message ? false : true);
}
});
},
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);
},
bind_add_to_cart_action() {
$('.page_content').on('click', '.btn-add-to-cart-list', (e) => {
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
this.animate_add_to_cart($btn);
const item_code = $btn.data('item-code');
erpnext.shopping_cart.update_cart({
item_code,
qty: 1
});
});
}
});

View File

@ -1,13 +1,12 @@
frappe.provide("erpnext.e_commerce");
var wishlist = erpnext.e_commerce;
frappe.provide("erpnext.wishlist");
var wishlist = erpnext.wishlist;
frappe.ready(function() {
$(".wishlist").toggleClass('hidden', true);
wishlist.set_wishlist_count();
});
frappe.provide("erpnext.shopping_cart");
var shopping_cart = erpnext.shopping_cart;
$.extend(wishlist, {
set_wishlist_count: function() {
// set badge count for wishlist icon
var wish_count = frappe.get_cookie("wish_count");
if(frappe.session.user==="Guest") {
wish_count = 0;
@ -35,5 +34,127 @@ $.extend(wishlist, {
} else {
$badge.remove();
}
},
bind_move_to_cart_action: function() {
// move item to cart from wishlist
$('.page_content').on("click", ".btn-add-to-cart", (e) => {
const $move_to_cart_btn = $(e.currentTarget);
let item_code = $move_to_cart_btn.data("item-code");
shopping_cart.shopping_cart_update({
item_code,
qty: 1,
cart_dropdown: true
});
let success_action = function() {
const $card_wrapper = $move_to_cart_btn.closest(".item-card");
$card_wrapper.addClass("wish-removed");
};
let args = { item_code: item_code };
this.add_remove_from_wishlist("remove", args, success_action, null, true);
});
},
bind_remove_action: function() {
// remove item from wishlist
$('.page_content').on("click", ".remove-wish", (e) => {
const $remove_wish_btn = $(e.currentTarget);
let item_code = $remove_wish_btn.data("item-code");
let success_action = function() {
const $card_wrapper = $remove_wish_btn.closest(".item-card");
$card_wrapper.addClass("wish-removed");
};
let args = { item_code: item_code };
this.add_remove_from_wishlist("remove", args, success_action);
});
},
bind_wishlist_action() {
// 'wish'('like') or 'unwish' item in product listing
$('.page_content').on('click', '.like-action', (e) => {
const $btn = $(e.currentTarget);
const $wish_icon = $btn.find('.wish-icon');
let me = this;
let success_action = function() {
erpnext.wishlist.set_wishlist_count();
};
if ($wish_icon.hasClass('wished')) {
// un-wish item
$btn.removeClass("like-animate");
this.toggle_button_class($wish_icon, 'wished', 'not-wished');
let args = { item_code: $btn.data('item-code') };
let failure_action = function() {
me.toggle_button_class($wish_icon, 'not-wished', 'wished');
};
this.add_remove_from_wishlist("remove", args, success_action, failure_action);
} else {
// wish item
$btn.addClass("like-animate");
this.toggle_button_class($wish_icon, 'not-wished', 'wished');
let args = {
item_code: $btn.data('item-code'),
price: $btn.data('price'),
formatted_price: $btn.data('formatted-price')
};
let failure_action = function() {
me.toggle_button_class($wish_icon, 'wished', 'not-wished');
};
this.add_remove_from_wishlist("add", args, success_action, failure_action);
}
});
},
toggle_button_class(button, remove, add) {
button.removeClass(remove);
button.addClass(add);
},
add_remove_from_wishlist(action, args, success_action, failure_action, async=false) {
/* AJAX call to add or remove Item from Wishlist
action: "add" or "remove"
args: args for method (item_code, price, formatted_price),
success_action: method to execute on successs,
failure_action: method to execute on failure,
async: make call asynchronously (true/false). */
let method = "erpnext.e_commerce.doctype.wishlist.wishlist.add_to_wishlist";
if (action === "remove") {
method = "erpnext.e_commerce.doctype.wishlist.wishlist.remove_from_wishlist";
}
frappe.call({
type: "POST",
method: method,
args: args,
callback: function (r) {
if (r.exc) {
if (failure_action && (typeof failure_action === 'function')) {failure_action();}
frappe.msgprint({
message: __("Sorry, something went wrong. Please refresh."),
indicator: "red", title: __("Note")
});
} else {
if (success_action && (typeof success_action === 'function')) {success_action();}
}
}
});
}
});
frappe.ready(function() {
if (window.location.pathname !== "/wishlist") {
$(".wishlist").toggleClass('hidden', true);
wishlist.set_wishlist_count();
} else {
wishlist.bind_move_to_cart_action();
wishlist.bind_remove_action();
}
});

View File

@ -648,3 +648,27 @@ body.product-page {
color: white;
}
}
.wishlist-cart-not-added {
color: var(--blue-500);
background-color: white;
border: 1px solid var(--blue-500);
--icon-stroke: var(--blue-500);
&:hover {
background-color: var(--blue-500);
color: white;
--icon-stroke: white;
}
}
.remove-wish {
&:hover {
background-color: var(--gray-100);
border: 1px solid var(--icon-stroke);
}
}
.wish-removed {
display: none;
}

View File

@ -118,7 +118,6 @@
'text-left': align == 'Left' or is_featured,
}) -%}
<div class="card-body {{ align_class }}" style="width:100%">
<div style="margin-top: 16px; display: flex;">
<a href="/{{ item.route or '#' }}">
<div class="product-title">{{ title or '' }}</div>
@ -128,7 +127,9 @@
{% endif %}
{% if not item.has_variants %}
<div class="like-action"
data-item-code="{{ item.item_code }}" data-price="{{ item.price }}">
data-item-code="{{ item.item_code }}"
data-price="{{ item.price }}"
data-formatted-price="{{ item.get('formatted_price') }}">
<svg class="icon sm">
{%- set icon_class = "wished" if item.wished else "not-wished"-%}
<use class="{{ icon_class }} wish-icon" href="#icon-heart"></use>
@ -161,3 +162,63 @@
{% endif %}
</div>
{%- endmacro -%}
{%- macro wishlist_card(item, settings) %}
<div class="col-sm-3 item-card" style="min-width: 220px;">
<div class="card text-center">
{% if item.image %}
<div class="card-img-container">
<a href="/{{ item.route or '#' }}" style="text-decoration: none;">
<img class="card-img" src="{{ item.image }}" alt="{{ title }}">
</a>
<div class="remove-wish"
style="position:absolute; top:10px; right: 20px; border-radius: 50%; border: 1px solid var(--gray-100); width: 25px; height: 25px;"
data-item-code="{{ item.item_code }}">
<span style="padding-bottom: 2px;">
<svg class="icon sm remove-wish-icon" style="margin-bottom: 4px; margin-left: 0.5px;">
<use class="close" href="#icon-close"></use>
</svg>
</span>
</div>
</div>
{% else %}
<a href="/{{ item.route or '#' }}" style="text-decoration: none;">
<div class="card-img-top no-image">
{{ frappe.utils.get_abbr(title) }}
</div>
</a>
{% endif %}
{{ wishlist_card_body(item, settings) }}
</div>
</div>
{%- endmacro -%}
{%- macro wishlist_card_body(item, settings) %}
<div class="card-body text-center" style="width:100%">
<div style="margin-top: 16px;">
<div class="product-title">{{ item.item_name or item.item_code or ''}}</div>
</div>
<div class="product-price">{{ item.formatted_price or '' }}</div>
{% if (item.available and settings.show_stock_availability) or (not settings.show_stock_availability) %}
<!-- Show move to cart button if in stock or if showing stock availability is disabled -->
<button data-item-code="{{ item.item_code}}" class="btn btn-add-to-cart w-100 wishlist-cart-not-added">
<span class="mr-2">
<svg class="icon icon-md">
<use href="#icon-assets"></use>
</svg>
</span>
{{ _("Move to Cart") }}
</button>
{% else %}
<div style="color: #F47A7A; width: 100%;">
{{ _("Not in Stock") }}
</div>
{% endif %}
</div>
{%- endmacro -%}

View File

@ -10,7 +10,7 @@
</a>
</li>
<li class="wishlist wishlist-icon hidden">
<a class="nav-link" href="/cart">
<a class="nav-link" href="/wishlist">
<svg class="icon icon-lg">
<use href="#icon-heart"></use>
</svg>

View File

@ -0,0 +1,24 @@
{% extends "templates/web.html" %}
{% block title %} {{ _("Wishlist") }} {% endblock %}
{% block header %}<h3 class="shopping-cart-header mt-2 mb-6">{{ _("Wishlist") }}</h1>{% endblock %}
{% block page_content %}
{% if items %}
<div class="row">
<div class="col-12 col-md-11 item-card-group-section">
<div class="row products-list">
{% from "erpnext/templates/includes/macros.html" import wishlist_card %}
{% for item in items %}
{{ wishlist_card(item, settings) }}
{% endfor %}
</div>
</div>
</div>
{% else %}
<!-- TODO: Make empty state for wishlist -->
{% include "erpnext/www/all-products/not_found.html" %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,39 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
no_cache = 1
import frappe
from erpnext.e_commerce.shopping_cart.cart import get_cart_quotation
def get_context(context):
settings = frappe.get_doc("E Commerce Settings")
items = get_wishlist_items()
if settings.show_stock_availability:
for item in items:
stock_qty = frappe.utils.flt(
frappe.db.get_value("Bin",
{
"item_code": item.item_code,
"warehouse": item.get("warehouse")
},
"actual_qty")
)
item.available = True if stock_qty else False
context.items = items
context.settings = settings
def get_wishlist_items():
if frappe.db.exists("Wishlist", frappe.session.user):
return frappe.db.sql("""
Select
item_code, item_name, website_item, price,
warehouse, image, item_group, route, formatted_price
from
`tabWishlist Items`
where
parent=%(user)s"""%{"user": frappe.db.escape(frappe.session.user)}, as_dict=1)
return

View File

@ -73,98 +73,8 @@ $(() => {
}
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);
this.animate_add_to_cart($btn);
const item_code = $btn.data('item-code');
erpnext.shopping_cart.update_cart({
item_code,
qty: 1
});
});
}
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);
}
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);
erpnext.shopping_cart.bind_add_to_cart_action();
erpnext.wishlist.bind_wishlist_action();
}
bind_search() {