Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> ProductReviews(string productId, Produc
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 */