feat: Recommended Items and Item full page refresh

- Added Optional Recommended Items
- Item Full Page minor UI Refresh
- Floating wishlist button in item full page
- Reviews section UI Refresh
This commit is contained in:
marination 2021-07-13 23:46:24 +05:30
parent b5e7f04b33
commit 2fec068aff
19 changed files with 690 additions and 274 deletions

View File

@ -27,6 +27,8 @@
"enable_wishlist",
"column_break_22",
"enable_reviews",
"column_break_23",
"enable_recommendations",
"section_break_18",
"company",
"price_list",
@ -367,12 +369,22 @@
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_23",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "enable_recommendations",
"fieldtype": "Check",
"label": "Enable Recommendations"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-07-07 21:32:17.363276",
"modified": "2021-07-13 16:30:14.715949",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "E Commerce Settings",

View File

@ -0,0 +1,87 @@
{
"actions": [],
"creation": "2021-07-12 20:52:12.503470",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"website_item",
"website_item_name",
"column_break_2",
"item_code",
"more_information_section",
"route",
"column_break_6",
"website_item_image",
"website_item_thumbnail"
],
"fields": [
{
"fieldname": "website_item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Website Item",
"options": "Website Item"
},
{
"fetch_from": "website_item.web_item_name",
"fieldname": "website_item_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Website Item Name",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information"
},
{
"fetch_from": "website_item.route",
"fieldname": "route",
"fieldtype": "Small Text",
"label": "Route",
"read_only": 1
},
{
"fetch_from": "website_item.image",
"fieldname": "website_item_image",
"fieldtype": "Attach",
"label": "Website Item Image",
"read_only": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fetch_from": "website_item.thumbnail",
"fieldname": "website_item_thumbnail",
"fieldtype": "Data",
"label": "Website Item Thumbnail",
"read_only": 1
},
{
"fetch_from": "website_item.item_code",
"fieldname": "item_code",
"fieldtype": "Data",
"label": "Item Code"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-13 21:02:19.031652",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Recommended Items",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class RecommendedItems(Document):
pass

View File

@ -39,6 +39,8 @@
"display_additional_information_section",
"show_tabbed_section",
"tabs",
"recommended_items_section",
"recommended_items",
"offers_section",
"offers",
"section_break_6",
@ -312,13 +314,25 @@
"fieldname": "short_description",
"fieldtype": "Small Text",
"label": "Short Website Description"
},
{
"collapsible": 1,
"fieldname": "recommended_items_section",
"fieldtype": "Section Break",
"label": "Recommended Items"
},
{
"fieldname": "recommended_items",
"fieldtype": "Table",
"label": "Recommended/Similar Items",
"options": "Recommended Items"
}
],
"has_web_view": 1,
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-07-11 20:49:45.415421",
"modified": "2021-07-12 21:00:04.065803",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Website Item",
@ -373,10 +387,10 @@
"write": 1
}
],
"search_fields": "item_code, item_name ,item_group",
"search_fields": "web_item_name, item_code, item_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "item_name",
"title_field": "web_item_name",
"track_changes": 1
}

View File

@ -13,6 +13,8 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide
from erpnext.setup.doctype.item_group.item_group import (get_parent_item_groups, invalidate_cache_for)
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
from erpnext.utilities.product import get_price
# SEARCH
from erpnext.e_commerce.website_item_indexing import (
@ -199,6 +201,12 @@ class WebsiteItem(WebsiteGenerator):
context.wished = True
context.user_is_customer = check_if_user_is_customer()
context.recommended_items = None
settings = context.shopping_cart.cart_settings
if settings.enable_recommendations:
context.recommended_items = self.get_recommended_items(settings)
return context
def set_variant_context(self, context):
@ -379,6 +387,38 @@ class WebsiteItem(WebsiteGenerator):
return tab_values
def get_recommended_items(self, settings):
items = frappe.db.sql(f"""
select
ri.website_item_thumbnail, ri.website_item_name,
ri.route, ri.item_code
from
`tabRecommended Items` ri, `tabWebsite Item` wi
where
ri.item_code = wi.item_code
and ri.parent = '{self.name}'
and wi.published = 1
order by ri.idx
""", as_dict=1)
if settings.show_price:
is_guest = frappe.session.user == "Guest"
# Show Price if logged in.
# If not logged in and price is hidden for guest, skip price fetch.
if is_guest and settings.hide_price_for_guest:
return items
selling_price_list = _set_price_list(settings, None)
for item in items:
item.price_info = get_price(
item.item_code,
selling_price_list,
settings.default_customer_group,
settings.company
)
return items
def invalidate_cache_for_web_item(doc):
"""Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager."""
from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website

View File

@ -11,6 +11,7 @@ WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict'
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE = 'website_items_category_dict'
ALLOWED_INDEXABLE_FIELDS_SET = {
'web_item_name',
'item_code',
'item_name',
'item_group',

View File

@ -176,6 +176,16 @@ $.extend(shopping_cart, {
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
if (frappe.session.user==="Guest") {
if (localStorage) {
localStorage.setItem("last_visited", window.location.pathname);
}
frappe.call('erpnext.e_commerce.api.get_guest_redirect_on_action').then((res) => {
window.location.href = res.message || "/login";
});
return;
}
$btn.addClass('hidden');
$btn.parent().find('.go-to-cart').removeClass('hidden');
$btn.parent().find('.go-to-cart-grid').removeClass('hidden');

View File

@ -81,53 +81,55 @@ $.extend(wishlist, {
// 'wish'('like') or 'unwish' item in product listing
$('.page_content').on('click', '.like-action, .like-action-list', (e) => {
const $btn = $(e.currentTarget);
const $wish_icon = $btn.find('.wish-icon');
let me = this;
if (frappe.session.user==="Guest") {
if (localStorage) {
localStorage.setItem("last_visited", window.location.pathname);
}
this.redirect_guest();
return;
}
let success_action = function() {
e_commerce.wishlist.set_wishlist_count();
};
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') };
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");
$btn.addClass("like-action-wished");
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);
}
this.wishlist_action($btn);
});
},
wishlist_action(btn) {
const $wish_icon = btn.find('.wish-icon');
let me = this;
if (frappe.session.user==="Guest") {
if (localStorage) {
localStorage.setItem("last_visited", window.location.pathname);
}
this.redirect_guest();
return;
}
let success_action = function() {
e_commerce.wishlist.set_wishlist_count();
};
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') };
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");
btn.addClass("like-action-wished");
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);

View File

@ -2,6 +2,7 @@
:root {
--green-info: #38A160;
--product-bg-color: white;
}
body.product-page {
@ -289,6 +290,7 @@ body.product-page {
.product-container {
@include card($padding: var(--padding-md));
background-color: var(--product-bg-color) !important;
min-height: 70vh;
.product-details {
@ -299,6 +301,12 @@ body.product-page {
}
}
&.item-main {
.product-image {
width: 100%;
}
}
.expand {
max-width: 100% !important; // expand in absence of slideshow
}
@ -327,9 +335,10 @@ body.product-page {
}
.product-title {
font-size: 24px;
font-size: 16px;
font-weight: 600;
color: var(--text-color);
padding: 0 !important;
}
.product-description {
@ -385,7 +394,7 @@ body.product-page {
.item-cart {
.product-price {
font-size: 20px;
font-size: 22px;
color: var(--text-color);
font-weight: 600;
@ -398,12 +407,94 @@ body.product-page {
.no-stock {
font-size: var(--text-base);
}
.offers-heading {
font-size: 16px !important;
color: var(--text-color);
.tag-icon {
--icon-stroke: var(--gray-500);
}
}
.w-30-40 {
width: 30%;
@media (max-width: 992px) {
width: 40%;
}
}
}
.tab-content {
font-size: 14px;
}
}
// Item Recommendations
.recommended-item-section {
padding-right: 0;
.recommendation-header {
font-size: 16px;
font-weight: 500
}
.recommendation-container {
padding: .5rem;
min-height: 0px;
.r-item-image {
width: 40%;
.no-image-r-item {
display: flex; justify-content: center;
background-color: var(--gray-200);
align-items: center;
color: var(--gray-400);
margin-top: .15rem;
border-radius: 6px;
height: 100%;
font-size: 24px;
}
}
.r-item-info {
font-size: 14px;
padding-left: 8px;
padding-right: 0;
width: 60%;
a {
color: var(--gray-800);
font-weight: 400;
}
.item-price {
font-size: 15px;
font-weight: 600;
color: var(--text-color);
}
.striked-item-price {
font-weight: 500;
color: var(--gray-500);
}
}
}
}
.product-code {
padding: .5rem 0;
color: var(--text-muted);
font-size: 14px;
.product-item-group {
padding-right: .25rem;
border-right: solid 1px var(--text-muted);
}
.product-item-code {
padding-left: .5rem;
}
}
.item-configurator-dialog {
@ -794,6 +885,12 @@ body.product-page {
}
}
.like-action-item-fp {
visibility: visible !important;
position: unset;
float: right;
}
.like-animate {
animation: expand cubic-bezier(0.04, 0.4, 0.5, 0.95) 1.6s forwards 1;
}
@ -919,16 +1016,63 @@ body.product-page {
.item-website-specification {
font-size: .875rem;
.product-title {
font-size: 18px;
}
.table {
width: 70%;
}
td {
border: none !important;
}
.spec-label {
color: var(--gray-600);
}
.spec-content {
color: var(--gray-800);
}
}
.reviews-full-page {
padding: 1rem 2rem;
}
.ratings-reviews-section {
border-top: 1px solid #E2E6E9;
padding: .5rem 1rem;
}
.reviews-header {
font-size: 20px;
font-weight: 600;
color: var(--gray-800);
display: flex;
align-items: center;
padding: 0;
}
.btn-write-review {
float: right;
padding: .5rem 1rem;
font-size: 14px;
font-weight: 400;
border: none !important;
box-shadow: none;
color: var(--gray-900);
background-color: var(--gray-100);
&:hover {
box-shadow: var(--btn-shadow);
}
}
.rating-summary-section {
display: flex;
}
.rating-summary-title {
@ -936,9 +1080,17 @@ body.product-page {
font-size: 18px;
}
.rating-summary-numbers {
display: flex;
flex-direction: column;
align-items: center;
border-right: solid 1px var(--gray-100);
}
.user-review-title {
margin-top: 0.15rem;
font-size: 16px;
font-size: 15px;
font-weight: 600;
}
@ -952,6 +1104,12 @@ body.product-page {
}
}
.ratings-pill {
background-color: var(--gray-100);
padding: .5rem 1rem;
border-radius: 66px;
}
.review {
max-width: 80%;
line-height: 1.6;
@ -961,21 +1119,18 @@ body.product-page {
.review-signature {
display: flex;
font-size: 14px;
font-size: 13px;
color: var(--gray-500);
font-weight: 400;
.reviewer {
padding-right: 8px;
margin-right: 8px;
border-right: 1px solid var(--gray-400);
color: var(--gray-600);
}
}
.rating-progress-bar-section {
padding-bottom: 2rem;
border-bottom: 1px solid #E2E6E9;
margin-right: -10px;
.rating-bar-title {
margin-left: -15px;
@ -985,14 +1140,15 @@ body.product-page {
margin-bottom: 4px;
height: 7px;
margin-top: 6px;
.progress-bar-cosmetic {
background-color: var(--gray-600);
border-radius: var(--border-radius);
}
}
}
.offer-container {
border: 1px solid var(--gray-300);
border-style: dashed;
border-radius: 4px;
padding: 6px;
font-size: 14px;
}
@ -1075,3 +1231,12 @@ body.product-page {
.font-md {
font-size: 14px !important;
}
.in-green {
color: var(--green-info) !important;
font-weight: 500;
}
.mt-minus-2 {
margin-top: -2rem;
}

View File

@ -1,4 +1,5 @@
{% extends "templates/web.html" %}
{% from "erpnext/templates/includes/macros.html" import recommended_item_row %}
{% block title %} {{ title }} {% endblock %}
@ -9,7 +10,7 @@
{% endblock %}
{% block page_content %}
<div class="product-container col-md-12">
<div class="product-container item-main">
{% from "erpnext/templates/includes/macros.html" import product_image %}
<div class="item-content">
<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
@ -18,33 +19,56 @@
{% include "templates/generators/item/item_image.html" %}
{% include "templates/generators/item/item_details.html" %}
</div>
<!-- Product Specifications Table Section -->
{% if show_tabs and tabs %}
<div class="category-tabs">
<!-- tabs -->
{{ web_block(
"Section with Tabs",
values=tabs,
add_container=0,
add_top_padding=0,
add_bottom_padding=0
) }}
</div>
{% elif website_specifications %}
{% include "templates/generators/item/item_specifications.html"%}
{% endif %}
<!-- Advanced Custom Website Content -->
{{ doc.website_content or '' }}
<!-- Reviews and Comments -->
{% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %}
{% include "templates/generators/item/item_reviews.html"%}
{% endif %}
</div>
</div>
</div>
<!-- Additional Info/Reviews, Recommendations -->
<div class="d-flex">
{% set show_recommended_items = recommended_items and shopping_cart.cart_settings.enable_recommendations %}
{% set info_col = 'col-9' if show_recommended_items else 'col-12' %}
{% set padding_top = 'pt-0' if (show_tabs and tabs) else '' %}
<div class="product-container mt-4 {{ padding_top }} {{ info_col }}">
<div class="item-content {{ 'mt-minus-2' if (show_tabs and tabs) else '' }}">
<div class="product-page-content" itemscope itemtype="http://schema.org/Product">
<!-- Product Specifications Table Section -->
{% if show_tabs and tabs %}
<div class="category-tabs">
<!-- tabs -->
{{ web_block("Section with Tabs", values=tabs, add_container=0,
add_top_padding=0, add_bottom_padding=0)
}}
</div>
{% elif website_specifications %}
{% include "templates/generators/item/item_specifications.html"%}
{% endif %}
<!-- Advanced Custom Website Content -->
{{ doc.website_content or '' }}
<!-- Reviews and Comments -->
{% if shopping_cart.cart_settings.enable_reviews and not doc.has_variants %}
{% include "templates/generators/item/item_reviews.html"%}
{% endif %}
</div>
</div>
</div>
<!-- Recommended Items -->
{% if show_recommended_items %}
<div class="mt-4 col-3 recommended-item-section">
<span class="recommendation-header">Recommended</span>
<div class="product-container mt-2 recommendation-container">
{% for item in recommended_items %}
{{ recommended_item_row(item) }}
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block base_scripts %}

View File

@ -7,34 +7,39 @@
<div class="col-md-12">
<!-- Price and Availability -->
{% if cart_settings.show_price and product_info.price %}
{% set price_info = product_info.price %}
{% set price_info = product_info.price %}
{% if price_info.formatted_mrp %}
<small class="formatted-price">
M.R.P.:
<s>{{ price_info.formatted_mrp }}</s>
</small>
<small class="ml-2 formatted-price" style="color: #F47A7A; font-weight: 500;">
{{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}} OFF
</small>
{% endif %}
<div class="product-price">
<!-- Final Price -->
{{ price_info.formatted_price_sales_uom }}
<div class="product-price">
{{ price_info.formatted_price_sales_uom }}
<small class="formatted-price">({{ price_info.formatted_price }} / {{ product_info.uom }})</small>
</div>
<!-- Striked Price and Discount -->
{% if price_info.formatted_mrp %}
<small class="formatted-price">
<s>MRP {{ price_info.formatted_mrp }}</s>
</small>
<small class="ml-1 formatted-price in-green">
-{{ price_info.get("formatted_discount_percent") or price_info.get("formatted_discount_rate")}}
</small>
{% endif %}
<!-- Price per UOM -->
<small class="formatted-price ml-2">
({{ price_info.formatted_price }} / {{ product_info.uom }})
</small>
</div>
{% else %}
{{ _("UOM") }} : {{ product_info.uom }}
{% endif %}
{% if cart_settings.show_stock_availability %}
<div>
<div class="mt-2">
{% if product_info.in_stock == 0 %}
<span class="text-danger no-stock">
{{ _('Not in stock') }}
</span>
{% elif product_info.in_stock == 1 %}
<span class="text-success has-stock">
<span class="in-green has-stock">
{{ _('In stock') }}
{% if product_info.show_stock_qty and product_info.stock_qty %}
({{ product_info.stock_qty[0][0] }})
@ -47,10 +52,15 @@
<!-- Offers -->
{% if doc.offers %}
<br>
<h3>Offers</h3>
<div class="offers-heading mb-4">
<span class="mr-1 tag-icon">
<svg class="icon icon-lg"><use href="#icon-tag"></use></svg>
</span>
<b>Available Offers</b>
</div>
<div class="offer-container">
{% for offer in doc.offers %}
<div class="mt-2" style="display: flex;">
<div class="mt-2 d-flex">
<div class="mr-2" >
<svg width="24" height="24" viewBox="0 0 24 24" stroke="var(--yellow-500)" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
@ -59,8 +69,8 @@
<path d="M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<p class="mr-1">
<strong>{{ _(offer.offer_title) }}:</strong>
<p class="mr-1 mb-1">
{{ _(offer.offer_title) }}:
{{ _(offer.offer_subtitle) }}
<a class="offer-details" href="#"
data-offer-title="{{ offer.offer_title }}" data-offer-id="{{ offer.name }}"
@ -74,20 +84,14 @@
{% endif %}
<!-- Add to Cart / View in Cart, Contact Us -->
<div class="mt-5 mb-5">
<div style="display: flex;" class="mb-4">
<div class="mt-6 mb-5">
<div class="mb-4 d-flex">
<!-- Add to Cart -->
{% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
<a href="/cart"
class="btn btn-light btn-view-in-cart hidden mr-2 font-md"
role="button"
>
<a href="/cart" class="btn btn-light btn-view-in-cart hidden mr-2 font-md" role="button">
{{ _("View in Cart") }}
</a>
<button
data-item-code="{{item_code}}"
class="btn btn-primary btn-add-to-cart w-50 mr-2"
>
<button data-item-code="{{item_code}}" class="btn btn-primary btn-add-to-cart mr-2 w-30-40">
<span class="mr-2">
<svg class="icon icon-md">
<use href="#icon-assets"></use>
@ -97,41 +101,11 @@
</button>
{% endif %}
<!-- Add to Wishlist -->
{% if cart_settings.enable_wishlist %}
<a href="/wishlist"
class="btn btn-view-in-wishlist font-md hidden"
role="button"
>
<span class="mr-2">
<svg class="icon icon-md">
<use href="#icon-heart"></use>
</svg>
</span>
{{ _("View in Wishlist") }}
</a>
{% set price = product_info.get("price") or {} %}
<button
data-item-code="{{item_code}}"
data-price="{{ price.get('price_list_rate') or 0}}"
data-formatted-price="{{ price.get('formatted_price') or 0 }}"
class="btn btn-add-to-wishlist font-md"
>
<span class="mr-2">
<svg class="icon icon-md">
<use href="#icon-heart"></use>
</svg>
</span>
{{ _("Add to Wishlist") }}
</button>
<!-- Contact Us -->
{% if cart_settings.show_contact_us_button %}
{% include "templates/generators/item/item_inquiry.html" %}
{% endif %}
</div>
<!-- Contact Us -->
{% if cart_settings.show_contact_us_button %}
{% include "templates/generators/item/item_inquiry.html" %}
{% endif %}
</div>
</div>
</div>
@ -155,28 +129,6 @@
});
});
$('.page_content').on('click', '.btn-add-to-wishlist', (e) => {
// Bind action on wishlist button
const $btn = $(e.currentTarget);
$btn.prop('disabled', true);
let args = {
item_code: $btn.data('item-code'),
price: $btn.data('price'),
formatted_price: $btn.data('formatted-price')
};
let failure_action = function() {
$btn.prop('disabled', false);
};
let success_action = function() {
$btn.prop('disabled', false);
e_commerce.wishlist.set_wishlist_count();
$('.btn-add-to-wishlist, .btn-view-in-wishlist').toggleClass('hidden');
};
e_commerce.wishlist.add_remove_from_wishlist("add", args, success_action, failure_action);
});
$('.page_content').on('click', '.offer-details', (e) => {
// Bind action on More link in Offers
const $btn = $(e.currentTarget);

View File

@ -1,28 +1,65 @@
{% set width_class = "expand" if not slides else "" %}
{% set cart_settings = shopping_cart.cart_settings %}
{% set product_info = shopping_cart.product_info %}
{% set price_info = product_info.get('price') or {} %}
<div class="col-md-7 product-details {{ width_class }}">
<!-- title -->
<h1 class="product-title" itemprop="name">
{{ doc.web_item_name }}
</h1>
<p class="product-code">
<span>{{ _("Item Code") }}:</span>
<span itemprop="productID">{{ doc.item_code }}</span>
</p>
{% if has_variants %}
<!-- configure template -->
{% include "templates/generators/item/item_configure.html" %}
{% else %}
<!-- add variant to cart -->
{% include "templates/generators/item/item_add_to_cart.html" %}
{% endif %}
<!-- description -->
<div class="product-description" itemprop="description">
{% if frappe.utils.strip_html(doc.web_long_description or '') %}
{{ doc.web_long_description | safe }}
{% elif frappe.utils.strip_html(doc.description or '') %}
{{ doc.description | safe }}
{% else %}
{{ _("No description given") }}
{% endif %}
</div>
<div class="d-flex">
<!-- title -->
<div class="product-title col-11" itemprop="name">
{{ doc.web_item_name }}
</div>
<!-- Wishlist -->
{% if cart_settings.enable_wishlist %}
<div class="like-action-item-fp like-action {{ 'like-action-wished' if wished else ''}} ml-2"
data-item-code="{{ doc.item_code }}"
data-price="{{ price_info.get('price_list_rate') if price_info else 0 }}"
data-formatted-price="{{ price_info.get('formatted_price') if price_info else 0 }}">
<svg class="icon sm">
<use class="{{ 'wished' if wished else 'not-wished' }} wish-icon" href="#icon-heart"></use>
</svg>
</div>
{% endif %}
</div>
<p class="product-code">
<span class="product-item-group">
{{ _(doc.item_group) }}
</span>
<span class="product-item-code">
{{ _("Item Code") }}:
</span>
<span itemprop="productID">{{ doc.item_code }}</span>
</p>
{% if has_variants %}
<!-- configure template -->
{% include "templates/generators/item/item_configure.html" %}
{% else %}
<!-- add variant to cart -->
{% include "templates/generators/item/item_add_to_cart.html" %}
{% endif %}
<!-- description -->
<div class="product-description" itemprop="description">
{% if frappe.utils.strip_html(doc.web_long_description or '') %}
{{ doc.web_long_description | safe }}
{% elif frappe.utils.strip_html(doc.description or '') %}
{{ doc.description | safe }}
{% else %}
{{ "" }}
{% endif %}
</div>
</div>
{% block base_scripts %}
<!-- js should be loaded in body! -->
<script type="text/javascript" src="/assets/frappe/js/lib/jquery/jquery.min.js"></script>
{% endblock %}
<script>
$('.page_content').on('click', '.like-action-item-fp', (e) => {
// Bind action on wishlist button
const $btn = $(e.currentTarget);
e_commerce.wishlist.wishlist_action($btn);
});
</script>

View File

@ -1,7 +1,7 @@
{% if shopping_cart and shopping_cart.cart_settings.enabled %}
{% set cart_settings = shopping_cart.cart_settings %}
{% if cart_settings.show_contact_us_button | int %}
<button class="btn btn-inquiry btn-primary-light font-md" data-item-code="{{ doc.name }}">
<button class="btn btn-inquiry font-md w-30-40" data-item-code="{{ doc.name }}">
{{ _('Contact Us') }}
</button>
{% endif %}

View File

@ -1,23 +1,29 @@
{% from "erpnext/templates/includes/macros.html" import user_review, ratings_summary %}
<div class="mt-12 ratings-reviews-section" style="display: flex;">
<div class="col-md-4 order-md-1 mt-8" style="max-width: 300px;">
{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }}
<div class="mt-4 ratings-reviews-section">
<!-- Title and Action -->
<div class="w-100 mt-4 mb-2 d-flex">
<div class="reviews-header col-9">
{{ _("Customer Reviews") }}
</div>
<div class="write-a-review-btn col-3">
<!-- Write a Review for legitimate users -->
{% if frappe.session.user != "Guest" and user_is_customer %}
<button class="btn btn-write-review"
data-web-item="{{ doc.name }}">
{{ _("Write a Review") }}
</button>
{% endif %}
</div>
</div>
<!-- Summary -->
{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }}
<!-- Write a Review for legitimate users -->
{% if frappe.session.user != "Guest" and user_is_customer %}
<button class="btn btn-light btn-write-review mr-2 mt-4 mb-4 w-100"
data-web-item="{{ doc.name }}">
{{ _("Write a Review") }}
</button>
{% endif %}
</div>
<!-- Reviews and Comments -->
<div class="col-12 order-2 col-md-9 order-md-2 mt-8 ml-16">
<h2 class="reviews-header">
{{ _("Reviews") }}
</h2>
<div class="mt-8">
{% if reviews %}
{{ user_review(reviews) }}
@ -64,7 +70,7 @@
callback: function(r) {
if(!r.exc) {
frappe.msgprint({
message: __("Thank you for submitting your review"),
message: __("Thank you for the review"),
title: __("Review Submitted"),
indicator: "green"
});

View File

@ -3,13 +3,15 @@
<div class="mt-5 item-website-specification">
<div class="col-md-11">
{% if not show_tabs %}
<h3 class="product-title mb-5 mt-8">Product Details</h3>
<div class="product-title mb-5 mt-8">
Product Details
</div>
{% endif %}
<table class="table table-bordered table-hover">
<table class="table">
{% for d in website_specifications -%}
<tr>
<td class="text-muted" style="width: 30%; font-weight: bold;">{{ d.label }}</td>
<td>{{ d.description }}</td>
<td class="spec-label">{{ d.label }}</td>
<td class="spec-content">{{ d.description }}</td>
</tr>
{%- endfor %}
</table>

View File

@ -7,8 +7,8 @@
</div>
{% endmacro %}
{% macro product_image(website_image, css_class="product-image", alt="") %}
<div class="border text-center rounded {{ css_class }}" style="overflow: hidden;">
{% macro product_image(website_image, css_class="product-image", alt="", no_border=False) %}
<div class="{{ 'border' if not no_border else ''}} text-center rounded {{ css_class }}" style="overflow: hidden;">
{% if website_image %}
<img itemprop="image" class="website-image h-100 w-100" alt="{{ alt }}" src="{{ frappe.utils.quoted(website_image) | abs_url }}">
{% else %}
@ -208,9 +208,12 @@
</div>
{%- endmacro -%}
{%- macro ratings_with_title(avg_rating, title, size, rating_header_class) -%}
<div style="display: flex;">
<div class="rating">
{%- macro ratings_with_title(avg_rating, title, size, rating_header_class, for_summary=False) -%}
<div class="{{ 'd-flex' if not for_summary else '' }}">
<p class="mr-4 {{ rating_header_class }}">
<span>{{ title }}</span>
</p>
<div class="rating {{ 'ratings-pill' if for_summary else ''}}">
{% for i in range(1,6) %}
{% set fill_class = 'star-click' if i <= avg_rating else '' %}
<svg class="icon icon-{{ size }} {{ fill_class }}">
@ -218,44 +221,50 @@
</svg>
{% endfor %}
</div>
<p class="ml-4 {{ rating_header_class }}">
<span>{{ title }}</span>
</p>
</div>
{%- endmacro -%}
{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating)-%}
<!-- Ratings Summary -->
<h2 class="reviews-header">
{{ _("Customer Ratings") }}
</h2>
{% if reviews %}
{% set rating_title = frappe.utils.cstr(average_rating) + " " + _("out of 5") %}
{{ ratings_with_title(average_whole_rating, rating_title, "lg", "rating-summary-title") }}
{% endif %}
<!-- Rating Progress Bars -->
<div class="rating-progress-bar-section">
{% for percent in reviews_per_rating %}
<div class="mt-4 col-sm-4 small rating-bar-title">
{{ loop.index }} star
{%- macro ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=False, total_reviews=None)-%}
<div class="rating-summary-section mt-4">
<div class="rating-summary-numbers col-3">
<h2 style="font-size: 2rem;">
{{ average_rating or 0 }}
</h2>
<div class="mb-2" style="margin-top: -.5rem;">
{{ frappe.utils.cstr(total_reviews) + " " + _("ratings") }}
</div>
<div class="row">
<div class="col-md-7">
<div class="progress rating-progress-bar" title="{{ percent }} % of reviews are {{ loop.index }} star">
<div class="progress-bar" role="progressbar"
aria-valuenow="{{ percent }}"
aria-valuemin="0" aria-valuemax="100"
style="width: {{ percent }}%; background-color: var(--text-on-green);">
<!-- Ratings Summary -->
{% if reviews %}
{% set rating_title = frappe.utils.cstr(average_rating) + " " + _("out of 5") if not for_summary else ''%}
{{ ratings_with_title(average_whole_rating, rating_title, "md", "rating-summary-title", for_summary) }}
{% endif %}
<div class="mt-2">{{ frappe.utils.cstr(average_rating or 0) + " " + _("out of 5") }}</div>
</div>
<!-- Rating Progress Bars -->
<div class="rating-progress-bar-section col-4 ml-4">
{% for percent in reviews_per_rating %}
<div class="col-sm-4 small rating-bar-title">
{{ loop.index }} star
</div>
<div class="row">
<div class="col-md-7">
<div class="progress rating-progress-bar" title="{{ percent }} % of reviews are {{ loop.index }} star">
<div class="progress-bar progress-bar-cosmetic" role="progressbar"
aria-valuenow="{{ percent }}"
aria-valuemin="0" aria-valuemax="100"
style="width: {{ percent }}%;">
</div>
</div>
</div>
<div class="col-sm-1 small">
{{ percent }}%
</div>
</div>
<div class="col-sm-1 small">
{{ percent }}%
</div>
</div>
{% endfor %}
{% endfor %}
</div>
</div>
{%- endmacro -%}
@ -264,17 +273,19 @@
<div class="user-reviews">
{% for review in reviews %}
<div class="mb-3 review">
{{ ratings_with_title(review.rating, _(review.review_title), "md", "user-review-title") }}
{{ ratings_with_title(review.rating, _(review.review_title), "sm", "user-review-title") }}
<div class="review-signature">
<span class="reviewer">{{ _(review.customer) }}</span>
<span>{{ review.published_on }}</span>
</div>
<div class="product-description mb-4 mt-4">
<div class="product-description mb-4">
<p>
{{ _(review.comment) }}
</p>
</div>
<div class="review-signature mb-2">
<span class="reviewer">{{ _(review.customer) }}</span>
<span class="indicator grey" style="--text-on-gray: var(--gray-300);"></span>
<span class="reviewer">{{ review.published_on }}</span>
</div>
</div>
{% endfor %}
</div>
@ -347,3 +358,42 @@
</div>
{% endfor %}
{%- endmacro -%}
{%- macro recommended_item_row(item)-%}
<div class="recommended-item mb-6 d-flex">
<div class="r-item-image">
{% if item.website_item_thumbnail %}
{{ product_image(item.website_item_thumbnail, alt="item.website_item_name", no_border=True) }}
{% else %}
<div class = "no-image-r-item">
{{ frappe.utils.get_abbr(item.website_item_name) or "NA" }}
</div>
{% endif %}
</div>
<div class="r-item-info">
<a href="/{{ item.route or '#'}}" target="_blank">
{% set title = item.website_item_name %}
{{ title[:70] + "..." if title|len > 70 else title }}
</a>
{% if item.get('price_info') %}
{% set price = item.get('price_info') %}
<div class="mt-2">
<span class="item-price">
{{ price.get('formatted_price') or '' }}
</span>
{% if price.get('formatted_mrp') %}
<br>
<span class="striked-item-price">
<s>MRP {{ price.formatted_mrp }}</s>
</span>
<span class="in-green">
- {{ price.get('formatted_discount_percent') or price.get('formatted_discount_rate')}}
</span>
{% endif %}
</div>
{% endif %}
</div>
</div>
{%- endmacro -%}

View File

@ -4,25 +4,30 @@
{% block title %} {{ _("Customer Reviews") }} {% endblock %}
{% block page_content %}
<div class="product-container col-md-12">
<div style="display: flex;">
<div class="col-md-4 order-md-1 mt-8" style="max-width: 300px;">
{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating) }}
<div class="product-container reviews-full-page col-md-12">
<!-- Title and Action -->
<div class="w-100 mb-6 d-flex">
<div class="reviews-header col-9">
{{ _("Customer Reviews") }}
</div>
<!-- Write a Review for legitimate users -->
{% if frappe.session.user != "Guest" %}
<button class="btn btn-light btn-write-review mr-2 mt-4 mb-4 w-100"
data-web-item="{{ web_item }}">
{{ _("Write a Review") }}
</button>
{% endif %}
<div class="write-a-review-btn col-3">
<!-- Write a Review for legitimate users -->
{% if frappe.session.user != "Guest" and user_is_customer %}
<button class="btn btn-write-review"
data-web-item="{{ web_item }}">
{{ _("Write a Review") }}
</button>
{% endif %}
</div>
</div>
<!-- Summary -->
{{ ratings_summary(reviews, reviews_per_rating, average_rating, average_whole_rating, for_summary=True, total_reviews=total_reviews) }}
<!-- Reviews and Comments -->
<div class="col-12 order-2 col-md-9 order-md-2 mt-8 ml-16">
<h2 class="reviews-header">
{{ _("Reviews") }}
</h2>
<div class="mt-8">
{% if reviews %}
{{ user_review(reviews) }}
@ -40,7 +45,6 @@
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import frappe
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
from erpnext.e_commerce.doctype.website_item.website_item import check_if_user_is_customer
def get_context(context):
context.no_cache = 1
@ -11,4 +12,5 @@ def get_context(context):
if frappe.form_dict and frappe.form_dict.get("item_code"):
context.item_code = frappe.form_dict.get("item_code")
context.web_item = frappe.db.get_value("Website Item", {"item_code": context.item_code}, "name")
context.user_is_customer = check_if_user_is_customer()
get_item_reviews(context.web_item, 0, 10, context)