feat: Animate Add to Cart List interactions (UX)
- Increased qty in cart on clicking add to cart for existing item - Simplified macro arguments - Navbar cart icon animation - Explore button for template item in card - Add to cart button animation
This commit is contained in:
parent
16b9c8c383
commit
4f64d1c7f2
@ -138,7 +138,7 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
|
|||||||
"additional_notes": additional_notes
|
"additional_notes": additional_notes
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
quotation_items[0].qty = qty
|
quotation_items[0].qty = qty + 1
|
||||||
quotation_items[0].additional_notes = additional_notes
|
quotation_items[0].additional_notes = additional_notes
|
||||||
|
|
||||||
apply_cart_settings(quotation=quotation)
|
apply_cart_settings(quotation=quotation)
|
||||||
@ -153,9 +153,8 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
|
|||||||
|
|
||||||
set_cart_count(quotation)
|
set_cart_count(quotation)
|
||||||
|
|
||||||
context = get_cart_quotation(quotation)
|
|
||||||
|
|
||||||
if cint(with_items):
|
if cint(with_items):
|
||||||
|
context = get_cart_quotation(quotation)
|
||||||
return {
|
return {
|
||||||
"items": frappe.render_template("templates/includes/cart/cart_items.html",
|
"items": frappe.render_template("templates/includes/cart/cart_items.html",
|
||||||
context),
|
context),
|
||||||
@ -164,8 +163,7 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
|
|||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
'name': quotation.name,
|
'name': quotation.name
|
||||||
'shopping_cart_menu': get_shopping_cart_menu(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@ -25,8 +25,7 @@
|
|||||||
{%- if item -%}
|
{%- if item -%}
|
||||||
{%- set item = frappe.get_doc("Item", item) -%}
|
{%- set item = frappe.get_doc("Item", item) -%}
|
||||||
{{ item_card(
|
{{ item_card(
|
||||||
item.item_name, item.image, item.route, item.description,
|
item, is_featured=values['card_' + index + '_featured'],
|
||||||
None, item.item_group, values['card_' + index + '_featured'],
|
|
||||||
True, "Center"
|
True, "Center"
|
||||||
) }}
|
) }}
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|||||||
@ -93,9 +93,6 @@ $.extend(shopping_cart, {
|
|||||||
btn: opts.btn,
|
btn: opts.btn,
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
shopping_cart.set_cart_count();
|
shopping_cart.set_cart_count();
|
||||||
if (r.message.shopping_cart_menu) {
|
|
||||||
$('.shopping-cart-menu').html(r.message.shopping_cart_menu);
|
|
||||||
}
|
|
||||||
if(opts.callback)
|
if(opts.callback)
|
||||||
opts.callback(r);
|
opts.callback(r);
|
||||||
}
|
}
|
||||||
@ -129,6 +126,10 @@ $.extend(shopping_cart, {
|
|||||||
|
|
||||||
if(cart_count) {
|
if(cart_count) {
|
||||||
$badge.html(cart_count);
|
$badge.html(cart_count);
|
||||||
|
$cart.addClass('cart-animate');
|
||||||
|
setTimeout(() => {
|
||||||
|
$cart.removeClass('cart-animate');
|
||||||
|
}, 500);
|
||||||
} else {
|
} else {
|
||||||
$badge.remove();
|
$badge.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -363,6 +363,31 @@ body.product-page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cart-animate {
|
||||||
|
animation: wiggle 0.5s linear;
|
||||||
|
}
|
||||||
|
@keyframes wiggle {
|
||||||
|
8%,
|
||||||
|
41% {
|
||||||
|
transform: translateX(-10px);
|
||||||
|
}
|
||||||
|
25%,
|
||||||
|
58% {
|
||||||
|
transform: translate(10px);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: translate(-5px);
|
||||||
|
}
|
||||||
|
92% {
|
||||||
|
transform: translate(5px);
|
||||||
|
}
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#page-cart {
|
#page-cart {
|
||||||
.shopping-cart-header {
|
.shopping-cart-header {
|
||||||
@ -557,16 +582,50 @@ body.product-page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-add-to-cart-list {
|
.btn-explore-variants {
|
||||||
|
box-shadow: none;
|
||||||
|
margin: var(--margin-sm) 0;
|
||||||
|
margin-left: 18px;
|
||||||
|
max-height: 30px; // to avoid resizing on window resize
|
||||||
|
flex: none;
|
||||||
|
transition: 0.3s ease;
|
||||||
|
color: var(--orange-500);
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid var(--orange-500);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: var(--orange-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-to-cart-list{
|
||||||
|
box-shadow: none;
|
||||||
|
margin: var(--margin-sm) 0;
|
||||||
|
max-height: 30px; // to avoid resizing on window resize
|
||||||
|
flex: none;
|
||||||
|
transition: 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-added {
|
||||||
|
margin-left: 18px;
|
||||||
color: var(--blue-500);
|
color: var(--blue-500);
|
||||||
background-color: white;
|
background-color: white;
|
||||||
box-shadow: none;
|
|
||||||
border: 1px solid var(--blue-500);
|
border: 1px solid var(--blue-500);
|
||||||
margin: var(--margin-sm) 0;
|
|
||||||
flex: none;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--blue-500);
|
background-color: var(--blue-500);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.added-to-cart {
|
||||||
|
margin-left: 18px;
|
||||||
|
background-color: var(--dark-green-400);
|
||||||
|
color: white;
|
||||||
|
border: 2px solid var(--green-300);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -59,13 +59,17 @@
|
|||||||
|
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{%- macro item_card(title, image, url, description, rate, category, in_stock=None, is_featured=False, is_full_width=False, align="Left") -%}
|
{%- macro item_card(item, is_featured=False, is_full_width=False, align="Left") -%}
|
||||||
{%- set align_items_class = resolve_class({
|
{%- set align_items_class = resolve_class({
|
||||||
'align-items-end': align == 'Right',
|
'align-items-end': align == 'Right',
|
||||||
'align-items-center': align == 'Center',
|
'align-items-center': align == 'Center',
|
||||||
'align-items-start': align == 'Left',
|
'align-items-start': align == 'Left',
|
||||||
}) -%}
|
}) -%}
|
||||||
{%- set col_size = 3 if is_full_width else 4 -%}
|
{%- set col_size = 3 if is_full_width else 4 -%}
|
||||||
|
{%- set title = item.item_name or item.item_code -%}
|
||||||
|
{%- set image = item.website_image or item.image -%}
|
||||||
|
{%- set description = item.website_description or item.description-%}
|
||||||
|
|
||||||
{% if is_featured %}
|
{% if is_featured %}
|
||||||
<div class="col-sm-{{ col_size*2 }} item-card">
|
<div class="col-sm-{{ col_size*2 }} item-card">
|
||||||
<div class="card featured-item {{ align_items_class }}">
|
<div class="card featured-item {{ align_items_class }}">
|
||||||
@ -75,12 +79,12 @@
|
|||||||
<img class="card-img" src="{{ image }}" alt="{{ title }}">
|
<img class="card-img" src="{{ image }}" alt="{{ title }}">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{{ item_card_body(title, description, url, rate, category, is_featured, align) }}
|
{{ item_card_body(title, description, item, is_featured, align) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{{ item_card_body(title, description, url, rate, category, is_featured, align) }}
|
{{ item_card_body(title, description, item, is_featured, align) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -90,24 +94,24 @@
|
|||||||
<div class="card {{ align_items_class }}">
|
<div class="card {{ align_items_class }}">
|
||||||
{% if image %}
|
{% if image %}
|
||||||
<div class="card-img-container">
|
<div class="card-img-container">
|
||||||
<a href="/{{ url or '#' }}" style="text-decoration: none;">
|
<a href="/{{ item.route or '#' }}" style="text-decoration: none;">
|
||||||
<img class="card-img" src="{{ image }}" alt="{{ title }}">
|
<img class="card-img" src="{{ image }}" alt="{{ title }}">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/{{ url or '#' }}" style="text-decoration: none;">
|
<a href="/{{ item.route or '#' }}" style="text-decoration: none;">
|
||||||
<div class="card-img-top no-image">
|
<div class="card-img-top no-image">
|
||||||
{{ frappe.utils.get_abbr(title) }}
|
{{ frappe.utils.get_abbr(title) }}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ item_card_body(title, description, url, rate, category, is_featured, align, in_stock) }}
|
{{ item_card_body(title, description, item, is_featured, align) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{%- endmacro -%}
|
{%- endmacro -%}
|
||||||
|
|
||||||
{%- macro item_card_body(title, description, url, rate, category, is_featured, align, in_stock=None) -%}
|
{%- macro item_card_body(title, description, item, is_featured, align) -%}
|
||||||
{%- set align_class = resolve_class({
|
{%- set align_class = resolve_class({
|
||||||
'text-right': align == 'Right',
|
'text-right': align == 'Right',
|
||||||
'text-center': align == 'Center' and not is_featured,
|
'text-center': align == 'Center' and not is_featured,
|
||||||
@ -116,33 +120,44 @@
|
|||||||
<div class="card-body {{ align_class }}" style="width:100%">
|
<div class="card-body {{ align_class }}" style="width:100%">
|
||||||
|
|
||||||
<div style="margin-top: 16px; display: flex;">
|
<div style="margin-top: 16px; display: flex;">
|
||||||
<a href="/{{ url or '#' }}">
|
<a href="/{{ item.route or '#' }}">
|
||||||
<div class="product-title">{{ title or '' }}</div>
|
<div class="product-title">{{ title or '' }}</div>
|
||||||
</a>
|
</a>
|
||||||
{% if in_stock %}
|
{% if item.in_stock %}
|
||||||
<span class="indicator {{ in_stock }} card-indicator"></span>
|
<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">
|
||||||
|
<svg class="icon sm">
|
||||||
|
<use class="wish-icon" href="#icon-heart"></use>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<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">
|
|
||||||
<svg class="icon sm">
|
|
||||||
<use class="wish-icon" href="#icon-heart"></use>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% if is_featured %}
|
{% if is_featured %}
|
||||||
<div class="product-price">{{ rate or '' }}</div>
|
<div class="product-price">{{ item.formatted_price or '' }}</div>
|
||||||
<div class="product-description ellipsis">{{ description or '' }}</div>
|
<div class="product-description ellipsis">{{ description or '' }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="product-category">{{ category or '' }}</div>
|
<div class="product-category">{{ item.item_group or '' }}</div>
|
||||||
<div style="display: flex;">
|
<div style="display: flex;">
|
||||||
{% if rate %}
|
{% if item.formatted_price %}
|
||||||
<div class="product-price" style="width: 60%;">{{ rate or '' }}</div>
|
<div class="product-price">{{ item.formatted_price or '' }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if item.has_variants %}
|
||||||
|
<a href="/{{ item.route or '#' }}">
|
||||||
|
<div class="btn btn-sm btn-explore-variants">
|
||||||
|
{{ _('Explore') }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div id="{{ item.name }}" class="btn btn-sm btn-add-to-cart-list not-added"
|
||||||
|
data-item-code="{{ item.item_code }}">
|
||||||
|
{{ _('Add to Cart') }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="btn btn-sm btn-add-to-cart-list">
|
|
||||||
{{ _('Add to Cart') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ $(() => {
|
|||||||
class ProductListing {
|
class ProductListing {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.bind_filters();
|
this.bind_filters();
|
||||||
|
this.bind_card_actions();
|
||||||
this.bind_search();
|
this.bind_search();
|
||||||
this.restore_filters_state();
|
this.restore_filters_state();
|
||||||
}
|
}
|
||||||
@ -71,8 +72,35 @@ $(() => {
|
|||||||
}, 1000));
|
}, 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
make_filters() {
|
bind_card_actions() {
|
||||||
|
$('.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;
|
||||||
|
button.removeClass('not-added');
|
||||||
|
button.addClass('added-to-cart');
|
||||||
|
$(btn_id).text('Added to Cart');
|
||||||
|
|
||||||
|
// undo
|
||||||
|
setTimeout(() => {
|
||||||
|
button.removeClass('added-to-cart');
|
||||||
|
button.addClass('not-added');
|
||||||
|
$(btn_id).text('Add to Cart');
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
bind_search() {
|
bind_search() {
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
|
{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
|
||||||
|
|
||||||
{{ item_card(
|
{{ item_card(item) }}
|
||||||
item.item_name or item.name, item.website_image or item.image, item.route, item.website_description or item.description,
|
|
||||||
item.formatted_price, item.item_group, in_stock=item.in_stock
|
|
||||||
) }}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user