Skip to content

Commit

Permalink
Minor improvements UX - add product reviews with modal popup (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
KrzysztofPajak committed Sep 30, 2021
1 parent 882bf77 commit 78ff072
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 90 deletions.
1 change: 1 addition & 0 deletions src/Web/Grand.Web/Controllers/ProductController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ public virtual async Task<IActionResult> NewProducts()
newmodel.AddProductReview.Rating = model.AddProductReview.Rating;
newmodel.AddProductReview.ReviewText = model.AddProductReview.ReviewText;
newmodel.AddProductReview.Title = model.AddProductReview.Title;
newmodel.AddProductReview.Result = string.Join(",", ModelState.Values.SelectMany(m => m.Errors).Select(e => e.ErrorMessage).ToList());

return View(newmodel);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,121 @@
@model ProductReviewsModel

<div class="mb-3">
<a class="btn btn-info" href="@Url.RouteUrl("ProductReviews", new { productId = Model.ProductId })">@Loc["Reviews.Overview.AddNew"]</a>
<a class="btn btn-info" @@click="addProductReview('@Url.RouteUrl("ProductReviews", new { productId = Model.ProductId })')">@Loc["Reviews.Overview.AddNew"]</a>
</div>

@if (Model.Items.Any())
@if (Model.AddProductReview.DisplayCaptcha)
{
<div id="product-review-list" class="product-review-list">
<div id="captcha-container">
<div class="captcha-box" id="captcha-box">
<captcha />
</div>
</div>
}
<partial name="_ProductReview.Modal" />
<template id="product-review-list" v-if="productreviews.Model !== null">
<div class="product-review-list">
<h5 class="mb-3"><strong>@Loc["Reviews.ExistingReviews"]</strong></h5>
@foreach (var review in Model.Items)
{
int ratingStars = review.Rating;
<div header-tag="header" footer-tag="footer" class="card product-review-item mb-3" data-url="@Url.RouteUrl("SetProductReviewHelpfulness")" data-id="@Model.ProductId" data-reviewid="@review.Id" data-title="@Loc["Reviews.Helpfulness.WasHelpful?"]">
<header class="card-header">
<template v-for="review in productreviews.Model">
<div class="card product-review-item mb-3">
<div class="card-header">
<div class="review-info d-inline-flex w-100">
<div class="user d-inline-flex align-items-center">
<small class="text-muted mr-2">@Loc["Reviews.From"]:</small>
<h6 class="mb-0">@review.CustomerName</h6>
<h6 class="mb-0">{{review.CustomerName}}</h6>
</div>
<b-icon icon="calendar2-check" variant="info" class="mx-2"></b-icon>
<small class="date text-muted">
<span>@Loc["Reviews.Date"]:</span>
<span>@review.WrittenOnStr</span>
<span class="ml-1">@Loc["Reviews.Date"]:</span>
<span>{{review.WrittenOnStr}}</span>
</small>
</div>
</header>
</div>
<div class="card-body">
<div class="review-title mb-3">
<h5 class="mb-0">@review.Title</h5>
<b-form-rating id='rating-@review.Id' class='p-0' variant='warning' no-border size='sm' show-value precision='2' readonly inline value='@(ratingStars)'></b-form-rating>
<h5 class="mb-0">{{review.Title}}</h5>
<b-form-rating id='rating-inline2' class='p-0' variant='warning' no-border size='sm' show-value precision='2' readonly inline :value='review.Rating'></b-form-rating>
</div>
<div class="review-content">
<div class="review-text">
@review.ReviewText
{{review.ReviewText}}
</div>
</div>
@if (!string.IsNullOrEmpty(review.ReplyText))
{
<hr />
<div class="reply-content mt-2">
<blockquote class="administration-response">
<h5 class="administration-response-header">@Loc["Reviews.AdministrationResponse"]</h5>
@review.ReplyText
<p>@review.Signature</p>
</blockquote>
</div>
}
</div>
<footer class="card-footer">
<partial name="_ProductReviewHelpfulness" model="review.Helpfulness" />
</footer>
<template v-if="review.ReplyText !== null">
<div class="reply-content">
<blockquote class="administration-response px-3">
<h5 class="administration-response-header">@Loc["Reviews.AdministrationResponse"]</h5>
<span>{{review.ReplyText}}</span>
<figcaption class="blockquote-footer">
{{review.Signature}}
</figcaption>
</blockquote>
</div>
</template>
<div class="card-footer">
<div class="product-review-helpfulness d-inline-flex justify-content-end align-items-center flex-wrap">
<span class="question">@Loc["Reviews.Helpfulness.WasHelpful?"]</span>
<span class="vote-options btn-group">
<span :id="'vote-yes-' + review.Helpfulness.ProductReviewId" @@click="productreviews.setProductReviewHelpfulness('@Url.RouteUrl("SetProductReviewHelpfulness")', 'true', review.Id, '@Model.ProductId', '@Loc[" Reviews.Helpfulness.WasHelpful?"]')" class="btn btn-sm btn-outline vote"><b-icon variant="success" icon="hand-thumbs-up"></b-icon></span>
<span :id="'vote-no-' + review.Helpfulness.ProductReviewId" @@click="productreviews.setProductReviewHelpfulness('@Url.RouteUrl("SetProductReviewHelpfulness")', 'false', review.Id, '@Model.ProductId', '@Loc[" Reviews.Helpfulness.WasHelpful?"]')" class="btn btn-sm btn-outline vote"><b-icon variant="danger" icon="hand-thumbs-down"></b-icon></span>
</span>
<span class="vote-stats d-inline-flex">
(<span :id="'helpfulness-vote-yes-' + review.Helpfulness.ProductReviewId">{{review.Helpfulness.HelpfulYesTotal}}</span>/<span :id="'helpfulness-vote-no-' + review.Helpfulness.ProductReviewId">{{review.Helpfulness.HelpfulNoTotal}}</span>)
</span>
</div>
</div>
</div>
}
</template>
<template v-else>
<p class="text-muted">@Loc["Products.Reviews.Empty"]</p>
</template>
</div>
}
else
{
<p class="text-muted">@Loc["Products.Reviews.Empty"]</p>
}
</template>

<script asp-location="Header">
var ProductReviews = Vue.extend({
data: function () {
return {
Model: null,
captcha: null
}
},
mounted: function() {
this.Model = @Json.Serialize(Model.Items);
},
methods: {
setProductReviewHelpfulness(url, wasHelpful, reviewId, productId, toastTitle) {
axios({
url: url,
method: 'post',
params: { "productReviewId": reviewId, "productId": productId, "washelpful": wasHelpful },
headers: {
'X-Response-View': 'Json'
},
}).then(function (response) {
document.getElementById("helpfulness-vote-yes-" + reviewId + "").innerHTML = response.data.TotalYes
document.getElementById("helpfulness-vote-no-" + reviewId + "").innerHTML = response.data.TotalNo;
new Vue({
el: ".modal-place",
methods: {
toast() {
this.$bvToast.toast(response.data.Result, {
title: toastTitle,
variant: 'info',
autoHideDelay: 3000,
solid: true
})
}
},
mounted: function () {
this.toast();
}
});
}).catch(function (error) {
alert(error);
})
}
}
});
var productreviews = new ProductReviews().$mount('#product-review-list');
</script>
41 changes: 41 additions & 0 deletions src/Web/Grand.Web/Views/Product/_ProductReview.Modal.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@{
var productlink = @Url.RouteUrl("Product");
}

<b-modal id="ModalProductReview" ref="ModalProductReview" :dark-theme="darkMode" size="xl" @@shown="modalReviewShown()" @@hide="modalReviewClose()" centered hide-footer hide-header>
<template v-if="PopupProductReviewVueModal !== null">
<h2 class="generalTitle h3">@Loc["Reviews.ProductReviewsFor"] <a :href="'@productlink' + PopupProductReviewVueModal.ProductSeName">{{PopupProductReviewVueModal.ProductName}}</a></h2>
<template v-if="!PopupProductReviewVueModal.AddProductReview.SuccessfullyAdded">
<div class="write-review" id="review-form">
<h5 class="generalTitle"><strong>@Loc["Reviews.Write"]</strong></h5>
<form id="addReviewForm" :action="'/productreviews/' + PopupProductReviewVueModal.ProductId" method="post" v-on:submit.prevent="validateBeforeSubmit($event)" :data-title="'@Loc["Reviews.ProductReviewsFor"] ' + PopupProductReviewVueModal.ProductName">
<input type="hidden" name="productId" :value="PopupProductReviewVueModal.ProductId" />
<div asp-validation-summary="ModelOnly" class="message-error alert alert-danger my-3"></div>
<fieldset>
<div class="form-fields">
<div class="form-group">
<label for="AddProductReview_Title" class="col-form-label">@Loc["Reviews.Fields.Title"]:</label>
<input data-val-required="@Loc["reviews.fields.title.required"]" id="AddProductReview_Title" name="AddProductReview.Title" class="form-control review-title" :disabled="!PopupProductReviewVueModal.AddProductReview.CanCurrentCustomerLeaveReview" v-validate="'required'" />
<span class="field-validation-error">{{veeErrors.first('AddProductReview.Title')}}</span>
</div>
<label for="AddProductReview_ReviewText" class="col-form-label">@Loc["Reviews.Fields.ReviewText"]:</label>
<textarea data-val-required="@Loc["reviews.fields.reviewtext.required"]" id="AddProductReview_ReviewText" name="AddProductReview.ReviewText" class="form-control review-text" :disabled="!PopupProductReviewVueModal.AddProductReview.CanCurrentCustomerLeaveReview" v-validate="'required'"></textarea>
<span class="field-validation-error">{{veeErrors.first('AddProductReview.ReviewText')}}</span>
<div class="form-group review-rating d-flex flex-wrap">
<label for="AddProductReview_Rating" class="col-form-label w-100">@Loc["Reviews.Fields.Rating"]:</label>
<b-form-rating v-model="value" variant="warning" show-value value="5" inline></b-form-rating>
<input class="sr-only" id="AddProductReview_Rating" name="AddProductReview.Rating" v-model="value" />
</div>
<template v-if="PopupProductReviewVueModal.AddProductReview.DisplayCaptcha">
<div id="captcha-popup"></div>
</template>
</div>
</fieldset>
<div class="buttons my-3">
<input type="button" @@click="submitProductReview('addReviewForm')" class="btn btn-info write-product-review-button" value="@Loc[" Reviews.SubmitButton"]" :disabled="!PopupProductReviewVueModal.AddProductReview.CanCurrentCustomerLeaveReview" />
</div>
</form>
</div>
</template>
</template>
</b-modal>
6 changes: 6 additions & 0 deletions src/Web/Grand.Web/wwwroot/theme/css/product/product.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@
max-width: 165px;
}

/* captcha */

#captcha-container .captcha-box {
display: none;
}

/* product price */

.product-details-page .overview .actual-price {
Expand Down
79 changes: 79 additions & 0 deletions src/Web/Grand.Web/wwwroot/theme/script/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
flycartfirstload: true,
PopupAddToCartVueModal: null,
PopupQuickViewVueModal: null,
PopupProductReviewVueModal: null,
index: null,
RelatedProducts: null,
}
Expand Down Expand Up @@ -348,6 +349,84 @@
}
}
}
},
addProductReview: function (url) {
axios({
url: url,
method: 'get',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Response-View': 'Json'
}
}).then(function (response) {
vm.PopupProductReviewVueModal = response.data;
vm.$refs['ModalProductReview'].show();
});
},
modalReviewShown: function () {
if (vm.PopupProductReviewVueModal.AddProductReview.DisplayCaptcha && document.querySelector("#ModalProductReview .captcha-box") == null) {
var html = document.getElementById("captcha-box");
document.getElementById("captcha-popup").prepend(html);
}
},
modalReviewClose: function () {
if (vm.PopupProductReviewVueModal.AddProductReview.DisplayCaptcha && document.querySelector("#ModalProductReview .captcha-box") !== null) {
var html = document.getElementById("captcha-box");
document.getElementById("captcha-container").prepend(html);
}
},
submitProductReview: function (id) {
this.$validator.validateAll(['AddProductReview.Title', 'AddProductReview.ReviewText', 'AddProductReview_Rating']).then((result) => {
if (result) {
var form = document.getElementById(id);
var url = form.getAttribute("action");
var resultTitle = form.getAttribute("data-title");
var bodyFormData = new FormData(form);
axios({
method: "post",
url: url,
data: bodyFormData,
headers: {
"Content-Type": "multipart/form-data",
'X-Response-View': 'Json'
},
}).then(function (response) {
vm.PopupProductReviewVueModal = response.data;
productreviews.Model = response.data.Items;

var result = response.data.AddProductReview.Result;
var variant = "";

if (response.data.AddProductReview.SuccessfullyAdded) {

variant = "info";
vm.$refs['ModalProductReview'].hide();

} else {
variant = "danger";
}

new Vue({
el: ".modal-place",
methods: {
toast() {
this.$bvToast.toast(result, {
title: resultTitle,
variant: variant,
autoHideDelay: 3000,
solid: true
})
}
},
mounted: function () {
this.toast();
}
});
});
return
}
});
}
},
});
55 changes: 1 addition & 54 deletions src/Web/Grand.Web/wwwroot/theme/script/public.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -967,57 +967,4 @@ var Reservation = {
}
}

/* END RESERVATION */

/* PRODUCT REVIEW */

function reviewHelpfullness(element) {
var productId = element.dataset.id;
var reviewId = element.dataset.reviewid;
var toastTitle = element.dataset.title;
var url = element.dataset.url;

document.getElementById('vote-yes-' + reviewId + '').addEventListener("click", function (e) {
setProductReviewHelpfulness(url, 'true');
});
document.getElementById('vote-no-' + reviewId + '').addEventListener("click", function (e) {
setProductReviewHelpfulness(url, 'false');
});

function setProductReviewHelpfulness(url, wasHelpful) {
axios({
url: url,
method: 'post',
params: { "productReviewId": reviewId, "productId": productId, "washelpful": wasHelpful }
}).then(function (response) {
document.getElementById("helpfulness-vote-yes-" + reviewId + "").innerHTML = response.data.TotalYes
document.getElementById("helpfulness-vote-no-" + reviewId + "").innerHTML = response.data.TotalNo;
new Vue({
el: ".modal-place",
methods: {
toast() {
this.$bvToast.toast(response.data.Result, {
title: toastTitle,
variant: 'info',
autoHideDelay: 3000,
solid: true
})
}
},
mounted: function () {
this.toast();
}
});
}).catch(function (error) {
alert(error);
})
}
}

document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll("#product-review-list .product-review-item").forEach(function (element) {
reviewHelpfullness(element);
});
})

/* END REVIEW */
/* END RESERVATION */

0 comments on commit 78ff072

Please sign in to comment.