diff --git a/assets/reviews.css b/assets/reviews.css index 72f10a57..077610a7 100644 --- a/assets/reviews.css +++ b/assets/reviews.css @@ -38,6 +38,11 @@ flex-direction: column; gap: 14px; } +@media (max-width: 768px) { + .yc-product-reviews .yc-reviews-wrapper .review-item { + padding-bottom: 12px; + } +} .yc-product-reviews .yc-reviews-wrapper .review-item .header { display: flex; flex-direction: column; @@ -107,3 +112,294 @@ border-radius: 5px; border: 1px solid #E6E6E6; } + +.modal { + display: none; + justify-content: center; + align-items: center; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.55); + z-index: 1000; +} +@media (max-width: 768px) { + .modal { + padding: 12px; + } +} +.modal .modal-content { + background-color: #fff; + position: relative; + width: 627px; +} +.modal .modal-content::-webkit-scrollbar { + width: 6px; + height: 6px; +} +.modal .modal-content::-webkit-scrollbar-thumb { + background-color: #555; + border-radius: 4px; +} +.modal .modal-content::-webkit-scrollbar-track { + background-color: #f1f1f1; + border-radius: 5px; +} +@media (max-width: 768px) { + .modal .modal-content { + max-height: 70vh; + overflow-y: scroll; + overflow-x: hidden; + } +} +.modal .modal-content .modal-header { + display: flex; + position: sticky; + top: 0; + background-color: white; + padding: 0; + margin: 0; + padding: 32px; +} +@media (max-width: 768px) { + .modal .modal-content .modal-header { + padding: 12px; + } +} +.modal .modal-content .modal-title { + font-size: 22px; + font-weight: 700; +} +.modal .modal-content .thank-you-message { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin: 15px auto 55px; + gap: 16px; +} +.modal .modal-content .thank-you-message .icon { + width: 66px; + height: 66px; +} +.modal .modal-content .thank-you-message h2 { + font-size: 30px; + font-weight: 800; +} +.modal .modal-content .thank-you-message p { + font-weight: 400; + font-size: 16px; +} +.modal .close { + color: #d1c4c4; + font-size: 32px; + cursor: pointer; + background-color: #fff; + height: 40px; + width: 40px; + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + position: absolute; + left: 0; + display: flex; + justify-content: center; + align-items: center; + margin-top: -10px; + margin-left: -40px; + padding-bottom: 4px; +} +[dir=ltr] .modal .close { + right: 0; + left: unset; + margin-left: unset; + margin-right: -40px; + border-top-left-radius: unset; + border-bottom-left-radius: unset; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} +.modal .close:hover, .modal .close:focus { + color: #000; + transition: all 0.2s ease-in-out; +} +@media (max-width: 768px) { + .modal .close { + margin: -10px auto 0 auto; + } +} + +#addReviewBtn { + margin: 40px auto 0 auto; +} + +#reviewForm { + display: flex; + flex-direction: column; + gap: 19px; + padding: 32px; +} +@media (max-width: 768px) { + #reviewForm { + padding: 16px 12px; + } +} +#reviewForm .inputs-wrapper { + display: flex; + gap: 10px; +} +#reviewForm .inputs-wrapper input { + width: 50%; +} +#reviewForm input, #reviewForm textarea { + padding: 16px; + border-radius: 4px; + border: 1px solid #000; + background: #FFF; + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.03); + resize: none; + outline: none; + font-family: var(--yc-font-family); +} +#reviewForm input::placeholder { + color: #A8A8A8; + font-size: 13px; + font-weight: 400; +} +#reviewForm .star-wrapper { + display: flex; + gap: 6px; + align-items: center; +} +#reviewForm .review-stars-count { + font-size: 13px; + font-weight: 400; + color: #707070; +} +#reviewForm .star-rating { + display: flex; + font-size: 0; + position: relative; + flex-direction: row-reverse; +} +#reviewForm .star-rating input[type=radio] { + display: none; +} +#reviewForm .star-rating input[type=radio]:checked ~ label, +#reviewForm .star-rating input[type=radio] + label:hover, +#reviewForm .star-rating input[type=radio] + label:hover ~ label { + color: var(--yc-reviews-stars-background); +} +#reviewForm .star-rating label { + color: #ccc; + cursor: pointer; + font-size: 24px; + margin: 0; + padding: 0 5px; + transition: color 0.2s; +} +#reviewForm .star-rating label:before { + content: "★"; +} +#reviewForm .yc-btn { + margin-top: 5px; +} + +.yc-upload-preview .yc-image img { + max-width: 100%; + height: auto; +} + +.yc-upload-preview:first-child { + display: none; +} + +.yc-image-preview-container { + display: flex; + flex-direction: row; + gap: 10px; + z-index: 2; + position: relative; +} + +.yc-image-preview img { + width: 60px; + height: 60px; + object-fit: cover; +} + +.yc-upload-wrapper .yc-image-preview:not(:first-child) { + width: 50px; + height: 50px; + margin-top: 10px; + position: relative; +} + +.yc-upload-wrapper .yc-image-preview:not(:first-child) img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.remove-button { + background-color: rgba(0, 0, 0, 0.5); + color: white; + border: none; + border-radius: 50%; + cursor: pointer; + width: 18px; + height: 18px; + margin-top: 3px; + display: flex; + align-items: center; + justify-content: center; +} + +.image-big-view { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.image-big-view img { + max-width: 80%; + max-height: 60vh; +} + +.yc-upload-preview .yc-image-preview:not(:first-child) { + width: 10px; + height: 10px; + position: relative; +} + +.uploaded-image { + width: 100%; + max-height: 120px; + object-fit: cover; +} + +.add-more { + margin-top: 8px; + font-size: 17px; + border: 1px solid #000; + width: 100%; + border-radius: 4px; +} + +.selected-image { + border: 2px solid #FDD07A; +} + +.field-error { + color: red; + font-size: 14px; + font-weight: 500; +} diff --git a/assets/reviews.js b/assets/reviews.js index aa7037bf..e6345d86 100644 --- a/assets/reviews.js +++ b/assets/reviews.js @@ -125,6 +125,255 @@ const setupReviews = async () => { } }; -document.addEventListener('DOMContentLoaded', () => { +const reviewData = { + content: '', + email: '', + ratings: 0, + first_name: '', + last_name: '', + images: [] +}; + +const uploadedImagesHistory = []; + +function setupEventListeners() { + const reviewForm = document.getElementById('reviewForm'); + const modal = document.getElementById("reviewModal"); + const btn = document.getElementById("addReviewBtn"); + const span = document.querySelector(".close"); + const starRadios = document.querySelectorAll('.star-rating input[type="radio"]'); + + reviewForm.addEventListener('submit', handleReviewFormSubmit); + btn.addEventListener('click', showModal); + span.addEventListener('click', hideModal); + + starRadios.forEach(radio => { + radio.addEventListener('change', function() { + const starCountSpan = document.querySelector('.review-stars-count'); + starCountSpan.textContent = `(${this.value} ${ratings})`; + }); + }); + setupReviews(); -}); +} + +async function handleReviewFormSubmit(e) { + e.preventDefault(); + clearFieldErrors(); + + const formData = new FormData(e.target); + Object.assign(reviewData, { + content: sanitizeInput(formData.get('content')), + email: formData.get('email'), + ratings: Number(formData.get('ratings')), + first_name: sanitizeInput(formData.get('first_name')), + last_name: sanitizeInput(formData.get('last_name')) + }); + + try { + const response = await youcanjs.product.submitReview(reviewsProductId, reviewData); + if (response) { + notify(`${REVIEWS_TRANSLATED_TEXT.successMessage}`, 'success'); + e.target.reset(); + e.target.style.display = 'none'; + document.querySelector('.thank-you-message').style.display = 'flex'; + document.querySelector('.modal-title').style.display = 'none'; + } else { + notify(`${REVIEWS_TRANSLATED_TEXT.errorMessage}`, 'error'); + } + } catch (error) { + handleReviewError(error); + } +} + +function handleReviewError(error) { + notify(error.message, 'error'); + if (error) { + for (const field in error.meta.fields) { + const errorMsg = error.meta.fields[field][0]; + displayFieldError(field, errorMsg); + } + } else { + notify(`${REVIEWS_TRANSLATED_TEXT.errorMessage}`, 'error'); + } +} + +function displayFieldError(fieldName, errorMsg) { + const inputElement = document.querySelector(`[name="${fieldName}"]`); + if (inputElement) { + const errorElement = document.createElement('span'); + errorElement.className = 'field-error'; + errorElement.textContent = errorMsg; + inputElement.parentElement.insertBefore(errorElement, inputElement.nextSibling); + } +} + +function clearFieldErrors() { + const errorElements = document.querySelectorAll('.field-error'); + errorElements.forEach(el => el.remove()); +} + +function showModal() { + const modal = document.getElementById("reviewModal"); + modal.style.display = "flex"; + document.body.style.overflow = 'hidden'; +} + +function hideModal() { + const modal = document.getElementById("reviewModal"); + modal.style.display = "none"; + document.body.style.overflow = ''; +} + +function sanitizeInput(input) { + const div = document.createElement('div'); + div.innerHTML = input; + return div.textContent || div.innerText || ""; +} + +function uploadReviewImage(container, event) { + const targetElement = event.target; + + const imgElement = container.querySelector('.uploaded-image'); + if (imgElement && imgElement.style.display === 'block' && targetElement !== container.querySelector('.add-more')) { + return; + } + + const uploadInput = createUploadInput(); + uploadInput.addEventListener('change', handleFileChange); + + function createUploadInput() { + const input = document.createElement('input'); + + input.type = 'file'; + input.accept = 'image/*'; + input.style.display = 'none'; + document.body.appendChild(input); + input.click(); + + return input; + } + + function handleFileChange(event) { + const files = event.target.files; + + if (files.length > 0) { + const file = files[0]; + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = async () => { + try { + const res = await youcanjs.product.upload(file); + if (res && res.link) { + reviewData.images.push(res.link); + uploadedImagesHistory.push(res.link); + + const imageUrl = reader.result; + displayUploadedImg(container, imageUrl); + appendImageToPreview(res.link, container.parentElement); + } + } catch (error) { + if (error.status === 422) { + const imageErrors = error.meta.fields.image; + if (imageErrors && imageErrors.length > 0) { + notify(imageErrors[0], 'error'); + } else { + notify(error.detail, 'error'); + } + } else { + notify(`${REVIEWS_TRANSLATED_TEXT.errorMessage}`, 'error'); + } + } + }; + } + } + } + +function displayUploadedImg(container, imageUrl) { + const imgElement = container.querySelector('.uploaded-image'); + imgElement.src = imageUrl; + imgElement.style.display = 'block'; + container.querySelector('.add-more').style.display = 'block'; + container.querySelector('.yc-upload').style.display = 'none'; +} + +function appendImageToPreview(imageUrl, parent) { + let container = parent.querySelector('.yc-image-preview-container'); + const submitButton = parent.querySelector('.yc-btn'); + + if (!container) { + container = document.createElement('div'); + container.className = 'yc-image-preview-container'; + parent.insertBefore(container, submitButton); + } + + const imagePreviewSection = document.createElement('div'); + const mainImage = document.querySelector('.uploaded-image'); + + imagePreviewSection.className = 'yc-image-preview'; + + const previewImage = document.createElement('img'); + previewImage.src = imageUrl; + previewImage.onclick = function() { + + mainImage.src = imageUrl; + mainImage.onclick = () => showExpandedImageView(mainImage); + parent.querySelectorAll('.yc-image-preview img').forEach(img => img.classList.remove('selected-image')); + previewImage.classList.add('selected-image'); +}; + + const deleteButton = createDeleteButton(imageUrl, imagePreviewSection); + imagePreviewSection.appendChild(previewImage); + imagePreviewSection.appendChild(deleteButton); + container.appendChild(imagePreviewSection); +} + +function createDeleteButton(imageUrl, parentElement) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = 'remove-button'; + button.innerHTML = 'X'; + + button.addEventListener('click', function() { + parentElement.remove(); + + const index = reviewData.images.indexOf(imageUrl); + if (index > -1) { + reviewData.images.splice(index, 1); + uploadedImagesHistory.splice(index, 1); + } + + if (uploadedImagesHistory.length > 0) { + const lastImage = uploadedImagesHistory[uploadedImagesHistory.length - 1]; + const mainImage = document.querySelector('.uploaded-image'); + mainImage.src = lastImage; + } else { + resetMainUploadContainerIfNoImages(); + } + }); + return button; +} + +function resetMainUploadContainerIfNoImages() { + if (reviewData.images.length === 0) { + const mainImage = document.querySelector('.uploaded-image'); + const container = mainImage.closest('.yc-upload-container'); + mainImage.src = ''; + mainImage.style.display = 'none'; + container.querySelector('.add-more').style.display = 'none'; + container.querySelector('.yc-upload').style.display = 'flex'; + } +} + +function showExpandedImageView(imgElement) { + const bigView = document.querySelector('.image-big-view'); + const bigImg = bigView.querySelector('img'); + bigImg.src = imgElement.src; + bigView.style.display = 'flex'; +} + +function hideExpandedImageView(bigViewElement) { + bigViewElement.style.display = 'none'; +} + +document.addEventListener('DOMContentLoaded', setupEventListeners); diff --git a/assets/upload-image.js b/assets/upload-image.js deleted file mode 100644 index 61d6f529..00000000 --- a/assets/upload-image.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Upload image input handler - * @param {HTMLElement} element - */ -function uploadImage(element) { - const parentSection = element.closest('.yc-single-product'); - const uploadInput = parentSection.querySelector('#yc-upload'); - const uploadArea = parentSection.querySelector('.yc-upload'); - const imagePreview = parentSection.querySelector('.yc-upload-preview'); - const imageWrapper = imagePreview.querySelector('.yc-image-preview'); - const progressContainer = imagePreview.querySelector('.progress-container'); - const imageName = $('.yc-image-info .image-name'); - const imageSize = $('.yc-image-info .image-size'); - const closePreviewButton = $('#close-preview'); - let uploadedImageLink = parentSection.querySelector('#yc-upload-link'); - - uploadInput.click(); - - uploadInput.addEventListener('change', async function () { - if (this.files && this.files[0]) { - const reader = new FileReader(); - reader.readAsDataURL(this.files[0]); - - reader.addEventListener("load", () => { - const base64 = reader.result; - const previews = parentSection.querySelectorAll('.yc-image-preview .yc-image img'); - - previews.forEach((preview) => { - preview.remove(); - }); - - uploadArea.style.display = 'none'; - imagePreview.style.display = 'block'; - imageWrapper.style.opacity = 0.4; - imageName.innerText = this.files[0].name; - progressContainer.style.display = "block"; - - const preview = document.createElement('img'); - preview.src = base64; - parentSection.querySelector('.yc-image-preview .yc-image').appendChild(preview); - - closePreviewButton.addEventListener('click', function () { - uploadArea.style.display = 'flex'; - imagePreview.style.display = 'none'; - uploadInput.value = ''; - preview.remove(); - }); - - getFileSize(this.files[0], preview); - smoothProgressBar(); - }); - - const res = await youcanjs.product.upload(this.files[0]); - if (res.error) return notify(res.error, 'error'); - - uploadedImageLink.value = res.link; - } - }); - - function getFileSize(file, source) { - const fileSizeInBytes = file.size; - const fileSizeInKB = fileSizeInBytes / 1024; - const fileSizeInMB = fileSizeInKB / 1024; - - if (fileSizeInMB > 2) { - source.src = ''; - source.style.height = "40px"; - imageName.style.color = "red"; - imageName.innerText = sizeBigMessage; - } - - if (fileSizeInMB < 1) { - return imageSize.innerText = fileSizeInKB.toFixed(2) + " Kb"; - } else { - return imageSize.innerText = fileSizeInMB.toFixed(2) + " Mb"; - } - } - - function smoothProgressBar() { - const progressBar = document.querySelector('.progress-bar'); - let progress = 0; - progressBar.style.width = progress; - const interval = setInterval(() => { - if (progress >= 100) { - setTimeout(() => { - progressContainer.style.display = 'none'; - imageWrapper.style.opacity = 1; - }, 1000); - clearInterval(interval); - return; - } - progress += 10; - progressBar.style.width = `${progress}%`; - }, 200); - - progressBar.style.transition = 'width 1s ease-in-out'; - } -} diff --git a/assets/youcan-js.min.js b/assets/youcan-js.min.js index 31c4f50e..007562a7 100644 --- a/assets/youcan-js.min.js +++ b/assets/youcan-js.min.js @@ -1 +1 @@ -(function(h,l){typeof exports=="object"&&typeof module<"u"?module.exports=l():typeof define=="function"&&define.amd?define(l):(h=typeof globalThis<"u"?globalThis:h||self,h["youcan-js"]=l())})(this,function(){"use strict";var tt=Object.defineProperty;var at=(h,l,d)=>l in h?tt(h,l,{enumerable:!0,configurable:!0,writable:!0,value:d}):h[l]=d;var i=(h,l,d)=>(at(h,typeof l!="symbol"?l+"":l,d),d);const h=n=>n,l=n=>{window.location.href=`${location.origin}${n}`},d=n=>n.map(a=>{try{return a?new URL(a).origin:void 0}catch{return`${window.location.protocol}//${a}`}}).filter(a=>typeof a<"u")[0];class s extends Error{constructor(a,e,c){super(a);i(this,"status");i(this,"detail");i(this,"meta");this.status=e,this.detail=a,this.meta=c,Object.setPrototypeOf(this,s.prototype)}}const D=async n=>{var t;try{if(!n.headers||((t=n.headers)==null?void 0:t.get("content-type"))!=="application/json")throw new s("The provided data is invalid",600);const a=await n.json();if(n.ok)return a;const e=h(a);throw e.error?new s(e.error||"",-7,[]):new s(e.detail,e.status,e.meta)}catch(a){throw a instanceof TypeError?new s(a.message,0):a instanceof s?a:new s(a.message,-1)}},v=class{constructor(t){i(this,"configs",{csrfToken:"",baseUrl:""});this.configs=t}static init(t){return this.instance===void 0&&t&&(this.instance=new v(t)),this.instance}get(){return this.configs}};let b=v;i(b,"instance");const{get:f,post:p,remove:_}=new class{constructor(){i(this,"request",async(t,a,e,c={isApi:!0})=>{const o=b.init(),u=o==null?void 0:o.get(),A=u==null?void 0:u.csrfToken;let w=u==null?void 0:u.baseUrl;const R={"x-csrf-token":String(A),"x-requested-with":"XMLHttpRequest",...c==null?void 0:c.headers};if(c.isApi&&(w=`${w}/api`),!w||!A)throw new s("Request failed duo to missing configs",-3);let T=e;e instanceof FormData||(R["Content-Type"]="application/json",T=JSON.stringify(e));const Z=await fetch(`${w}${a}`,{method:t,headers:R,body:T});return D(Z)});i(this,"get",(t,a)=>this.request("GET",t,void 0,a));i(this,"post",(t,a,e)=>this.request("POST",t,a,e));i(this,"remove",(t,a,e)=>this.request("DELETE",t,a,e))}},y=async(n,t)=>{const a=c=>Object.entries(c).map(([o,u])=>`${o}=${u}`).join("&");let e="";for(const[c,o]of Object.entries(t))c!=="filters"?e+=`${c}=${o}&`:o.forEach(u=>{e+=`${a(u)}&`});return await f(`${n}?${e}`)},r=async n=>{try{return{ok:!0,data:await n()}}catch(t){return t instanceof s?{ok:!1,data:t}:{ok:!1,data:new s(t.message,-2)}}};class M{async fetch(t){return await r(async()=>await f(`/products/${t}`))}async fetchByCategory(t,a){return await r(async()=>await y(`/categories/${t}/products`,a))}async fetchByVariantId(t){return await r(async()=>await f(`/products/variant/${t}`))}async fetchAll(t){return await r(async()=>await y("/products",t))}async fetchReviews(t,a){return await r(async()=>await y(`/products/${t}/reviews`,a))}async upload(t){const a=new FormData;if(!(typeof t=="string"?JSON.parse(t):t).type.includes("image"))throw new s("The file type is not an image",-6);return a.append("image",t),await r(async()=>await p("/upload/image",a,{isApi:!1}))}}class C{async fetch(t){return await r(async()=>await f(`/categories/${t}`))}async fetchAll(t){return await r(async()=>await y("/categories",t))}async fetchSubCategories(t,a){return await r(async()=>await y(`/categories/${t}/children`,a))}}class j{async fetch(){return await r(async()=>await f("/cart/content"))}async addItem(t){return await r(async()=>await p("/cart/add",{quantity:t.quantity,attachedImage:t.attachedImage,id:t.productVariantId}))}async removeItem(t){return await r(async()=>await p("/cart/remove",{id:t.cartItemId,productVariantId:t.productVariantId}))}async updateItem(t){return await r(async()=>await p("/cart/update",{quantity:t.quantity,productVariantId:t.productVariantId,id:t.cartItemId}))}async note(t){return await r(async()=>await p("/checkout/notes",{additional_note:t},{isApi:!1}))}}class x{async placeExpressCheckoutOrder(t){const a=new FormData;a.append("id",t.productVariantId),a.append("quantity",String(t.quantity));for(const c in t.fields)a.append(c,t.fields[c]);return await r(async()=>await p("/checkout/express",a))}async applyCoupon(t){return await r(async()=>await p("/checkout/coupons",{coupon:t},{isApi:!1}))}async removeCoupons(){return await r(async()=>await _("/checkout/coupons",void 0,{isApi:!1}))}}class B{async fetch(){return await r(async()=>await f("/menus"))}}let U=class{async contact(t){return await r(async()=>await p("/contact",t))}},V=class{async fetchAll(t){return await r(async()=>await y("/pages",t))}};class P{async answer(t){const a=new FormData;return a.append("answer",t.answer),a.append("order_id",t.order_id),Object.entries(t.product_offers).forEach(([c,o])=>{a.append(`product_offers[${c}]`,o)}),await r(async()=>await p(`/upsells/${t.upsell_id}/answer`,a))}}const{productApi:k,categoryApi:S,cartApi:m,checkoutApi:E,menuApi:F,storeApi:L,pageApi:N,upsellApi:G}={productApi:new M,categoryApi:new C,cartApi:new j,checkoutApi:new x,menuApi:new B,storeApi:new U,pageApi:new V,upsellApi:new P};class g{constructor(t,a,e){i(this,"callback");i(this,"queryOptions");i(this,"meta");this.callback=t,this.queryOptions=a,this.meta=e}async data(){const{data:t}=await this.callback(this.queryOptions);if(t instanceof s)throw t;return this.meta=t.meta,t.data}pagination(){var t,a,e;return(t=this.meta)!=null&&t.pagination?{totalPages:(a=this.meta)==null?void 0:a.pagination.total_pages,currentPage:(e=this.meta)==null?void 0:e.pagination.current_page}:{totalPages:2,currentPage:1}}next(){const{currentPage:t,totalPages:a}=this.pagination();return this.queryOptions.page=t1?t-1:t,this}orderBy(t="",a=""){return this.queryOptions.sort_field=t,this.queryOptions.sort_order=a,this}search(t=""){return this.queryOptions.q=t,this}filterBy(t,a,e="="){const c=this.queryOptions.filters||[];return c.push({[`filters[${c.length}][field]`]:t,[`filters[${c.length}][operator]`]:e,[`filters[${c.length}][value]`]:a}),this.queryOptions.filters=c,this}}class H{fetchAll(t={}){const a=async e=>await k.fetchAll(e);return new g(a,t)}async fetchByIdOrSlug(t){const{data:a}=await k.fetch(t);if(a instanceof s)throw a;return a}fetchByCategory(t,a={}){const e=async c=>await k.fetchByCategory(t,c);return new g(e,a)}async fetchByVariantId(t){const{data:a}=await k.fetchByVariantId(t);if(a instanceof s)throw a;return a}fetchReviews(t,a={}){const e=async c=>await k.fetchReviews(t,c);return new g(e,a)}async upload(t){const{data:a}=await k.upload(t);if(a instanceof s)throw a;return a}}class K{async fetch(t){const{data:a}=await S.fetch(t);if(a instanceof s)throw a;return a}fetchAll(t={}){const a=async e=>await S.fetchAll(e);return new g(a,t)}fetchSubCategories(t,a={}){const e=async c=>await S.fetchSubCategories(t,c);return new g(e,a)}}class O{async fetch(){const{data:t}=await m.fetch();if(t instanceof s)throw t;return t}async addItem(t){const{quantity:a,productVariantId:e,attachedImage:c=null}=t,{data:o}=await m.addItem({quantity:a,attachedImage:c,productVariantId:e});if(o instanceof s)throw o;return await this.fetch()}async removeItem(t){const{data:a}=await m.removeItem(t);if(a instanceof s)throw a;return await this.fetch()}async updateItem(t){const{data:a}=await m.updateItem(t);if(a instanceof s)throw a;return await this.fetch()}async note(t){const{data:a}=await m.note(t);if(a instanceof s)throw a}}const $=new O;class J{constructor(t,a){i(this,"data");i(this,"ok");i(this,"ERROR_MESSAGES",{INVALID_DATA:"The given data was invalid.",SKIP_SHIPPING:"cannot skip shipping step",SKIP_PAYMENT:"cannot skip payment step"});this.data=t,this.ok=a}onSuccess(t){return this.ok&&!(this.data instanceof s)&&t(this.data,()=>l("/checkout/thankyou")),this}onValidationErr(t){return this.data instanceof s&&this.data.detail===this.ERROR_MESSAGES.INVALID_DATA&&t(this.data),this}onSkipShippingStep(t){return this.data instanceof s&&this.data.detail===this.ERROR_MESSAGES.SKIP_SHIPPING&&t(this.data,()=>l("/checkout/shipping")),this}onSkipPaymentStep(t){return this.data instanceof s&&this.data.detail===this.ERROR_MESSAGES.SKIP_PAYMENT&&t(this.data,()=>l("/checkout/payment")),this}catch(t){return this.data instanceof s&&!this.ok&&t(this.data),this}finally(t){return t(),this}}class q{async placeExpressCheckoutOrder(t){const{data:a,ok:e}=await E.placeExpressCheckoutOrder(t);return new J(a,e)}async applyCoupon(t){const{data:a}=await E.applyCoupon(t);if(a instanceof s)throw a;return $.fetch()}async removeCoupons(){const{data:t}=await E.removeCoupons();if(t instanceof s)throw t;return $.fetch()}}i(q,"onSuccess");class Q{async fetch(){const{data:t}=await F.fetch();if(t instanceof s)throw t;return t}}class Y{async contact(t){const{data:a}=await L.contact(t);if(a instanceof s)throw a}}class z{fetchAll(t={}){const a=async e=>await N.fetchAll(e);return new g(a,t)}}class X{async answer(t){const{data:a}=await G.answer(t);if(a instanceof s)throw a;return a}}const W={BASE_URL:""};class I{constructor(){i(this,"product");i(this,"category");i(this,"cart");i(this,"checkout");i(this,"menu");i(this,"store");i(this,"page");i(this,"upsell");this.product=new H,this.category=new K,this.cart=new O,this.checkout=new q,this.menu=new Q,this.store=new Y,this.page=new z,this.upsell=new X}static init(){var o,u,A,w;if(typeof window.Dotshop>"u")throw new TypeError("Failed to initialize due to missing store context");const t=(o=window.Dotshop.store)==null?void 0:o.domain,a=(u=window.location)==null?void 0:u.hostname,c=[t!==a&&a!=="seller-area.youcan.shop"?(A=window.location)==null?void 0:A.origin:(w=window.Dotshop.store)==null?void 0:w.domain,W.BASE_URL];return b.init({csrfToken:window.Dotshop.csrfToken,baseUrl:d(c)}),new I}}return window.youcanjs=I.init(),I}); +(function(h,l){typeof exports=="object"&&typeof module<"u"?module.exports=l():typeof define=="function"&&define.amd?define(l):(h=typeof globalThis<"u"?globalThis:h||self,h["youcan-js"]=l())})(this,function(){"use strict";var tt=Object.defineProperty;var at=(h,l,d)=>l in h?tt(h,l,{enumerable:!0,configurable:!0,writable:!0,value:d}):h[l]=d;var i=(h,l,d)=>(at(h,typeof l!="symbol"?l+"":l,d),d);const h=c=>c,l=c=>{window.location.href=`${location.origin}${c}`},d=c=>c.map(a=>{try{return a?new URL(a).origin:void 0}catch{return`${window.location.protocol}//${a}`}}).filter(a=>typeof a<"u")[0];class n extends Error{constructor(a,e,s){super(a);i(this,"status");i(this,"detail");i(this,"meta");this.status=e,this.detail=a,this.meta=s,Object.setPrototypeOf(this,n.prototype)}}const D=async c=>{var t;try{if(!c.headers||((t=c.headers)==null?void 0:t.get("content-type"))!=="application/json")throw new n("The provided data is invalid",600);const a=await c.json();if(c.ok)return a;const e=h(a);throw e.error?new n(e.error||"",-7,[]):new n(e.detail,e.status,e.meta)}catch(a){throw a instanceof TypeError?new n(a.message,0):a instanceof n?a:new n(a.message,-1)}},E=class{constructor(t){i(this,"configs",{csrfToken:"",baseUrl:""});this.configs=t}static init(t){return this.instance===void 0&&t&&(this.instance=new E(t)),this.instance}get(){return this.configs}};let g=E;i(g,"instance");const{get:y,post:p,remove:_}=new class{constructor(){i(this,"request",async(t,a,e,s={isApi:!0})=>{const o=g.init(),u=o==null?void 0:o.get(),A=u==null?void 0:u.csrfToken;let f=u==null?void 0:u.baseUrl;const R={"x-csrf-token":String(A),"x-requested-with":"XMLHttpRequest",...s==null?void 0:s.headers};if(s.isApi&&(f=`${f}/api`),!f||!A)throw new n("Request failed duo to missing configs",-3);let T=e;e instanceof FormData||(R["Content-Type"]="application/json",T=JSON.stringify(e));const Z=await fetch(`${f}${a}`,{method:t,headers:R,body:T});return D(Z)});i(this,"get",(t,a)=>this.request("GET",t,void 0,a));i(this,"post",(t,a,e)=>this.request("POST",t,a,e));i(this,"remove",(t,a,e)=>this.request("DELETE",t,a,e))}},k=async(c,t)=>{const a=s=>Object.entries(s).map(([o,u])=>`${o}=${u}`).join("&");let e="";for(const[s,o]of Object.entries(t))s!=="filters"?e+=`${s}=${o}&`:o.forEach(u=>{e+=`${a(u)}&`});return await y(`${c}?${e}`)},r=async c=>{try{return{ok:!0,data:await c()}}catch(t){return t instanceof n?{ok:!1,data:t}:{ok:!1,data:new n(t.message,-2)}}};class M{async fetch(t){return await r(async()=>await y(`/products/${t}`))}async fetchByCategory(t,a){return await r(async()=>await k(`/categories/${t}/products`,a))}async fetchByVariantId(t){return await r(async()=>await y(`/products/variant/${t}`))}async fetchAll(t){return await r(async()=>await k("/products",t))}async fetchReviews(t,a){return await r(async()=>await k(`/products/${t}/reviews`,a))}async upload(t){const a=new FormData;if(!(typeof t=="string"?JSON.parse(t):t).type.includes("image"))throw new n("The file type is not an image",-6);return a.append("image",t),await r(async()=>await p("/upload/image",a,{isApi:!1}))}async submitReview(t,a){return await r(async()=>await p(`/products/${t}/reviews`,a))}}class C{async fetch(t){return await r(async()=>await y(`/categories/${t}`))}async fetchAll(t){return await r(async()=>await k("/categories",t))}async fetchSubCategories(t,a){return await r(async()=>await k(`/categories/${t}/children`,a))}}class j{async fetch(){return await r(async()=>await y("/cart/content"))}async addItem(t){return await r(async()=>await p("/cart/add",{quantity:t.quantity,attachedImage:t.attachedImage,id:t.productVariantId}))}async removeItem(t){return await r(async()=>await p("/cart/remove",{id:t.cartItemId,productVariantId:t.productVariantId}))}async updateItem(t){return await r(async()=>await p("/cart/update",{quantity:t.quantity,productVariantId:t.productVariantId,id:t.cartItemId}))}async note(t){return await r(async()=>await p("/checkout/notes",{additional_note:t},{isApi:!1}))}}class x{async placeExpressCheckoutOrder(t){const a=new FormData;a.append("id",t.productVariantId),a.append("quantity",String(t.quantity));for(const s in t.fields)a.append(s,t.fields[s]);return await r(async()=>await p("/checkout/express",a))}async applyCoupon(t){return await r(async()=>await p("/checkout/coupons",{coupon:t},{isApi:!1}))}async removeCoupons(){return await r(async()=>await _("/checkout/coupons",void 0,{isApi:!1}))}}class B{async fetch(){return await r(async()=>await y("/menus"))}}let U=class{async contact(t){return await r(async()=>await p("/contact",t))}},V=class{async fetchAll(t){return await r(async()=>await k("/pages",t))}};class P{async answer(t){const a=new FormData;return a.append("answer",t.answer),a.append("order_id",t.order_id),Object.entries(t.product_offers).forEach(([s,o])=>{a.append(`product_offers[${s}]`,o)}),await r(async()=>await p(`/upsells/${t.upsell_id}/answer`,a))}}const{productApi:w,categoryApi:S,cartApi:m,checkoutApi:v,menuApi:F,storeApi:L,pageApi:N,upsellApi:G}={productApi:new M,categoryApi:new C,cartApi:new j,checkoutApi:new x,menuApi:new B,storeApi:new U,pageApi:new V,upsellApi:new P};class b{constructor(t,a,e){i(this,"callback");i(this,"queryOptions");i(this,"meta");this.callback=t,this.queryOptions=a,this.meta=e}async data(){const{data:t}=await this.callback(this.queryOptions);if(t instanceof n)throw t;return this.meta=t.meta,t.data}pagination(){var t,a,e;return(t=this.meta)!=null&&t.pagination?{totalPages:(a=this.meta)==null?void 0:a.pagination.total_pages,currentPage:(e=this.meta)==null?void 0:e.pagination.current_page}:{totalPages:2,currentPage:1}}next(){const{currentPage:t,totalPages:a}=this.pagination();return this.queryOptions.page=t1?t-1:t,this}orderBy(t="",a=""){return this.queryOptions.sort_field=t,this.queryOptions.sort_order=a,this}search(t=""){return this.queryOptions.q=t,this}filterBy(t,a,e="="){const s=this.queryOptions.filters||[];return s.push({[`filters[${s.length}][field]`]:t,[`filters[${s.length}][operator]`]:e,[`filters[${s.length}][value]`]:a}),this.queryOptions.filters=s,this}}class H{fetchAll(t={}){const a=async e=>await w.fetchAll(e);return new b(a,t)}async fetchByIdOrSlug(t){const{data:a}=await w.fetch(t);if(a instanceof n)throw a;return a}fetchByCategory(t,a={}){const e=async s=>await w.fetchByCategory(t,s);return new b(e,a)}async fetchByVariantId(t){const{data:a}=await w.fetchByVariantId(t);if(a instanceof n)throw a;return a}fetchReviews(t,a={}){const e=async s=>await w.fetchReviews(t,s);return new b(e,a)}async upload(t){const{data:a}=await w.upload(t);if(a instanceof n)throw a;return a}async submitReview(t,a){const{data:e}=await w.submitReview(t,a);if(e instanceof n)throw e;return e}}class K{async fetch(t){const{data:a}=await S.fetch(t);if(a instanceof n)throw a;return a}fetchAll(t={}){const a=async e=>await S.fetchAll(e);return new b(a,t)}fetchSubCategories(t,a={}){const e=async s=>await S.fetchSubCategories(t,s);return new b(e,a)}}class O{async fetch(){const{data:t}=await m.fetch();if(t instanceof n)throw t;return t}async addItem(t){const{quantity:a,productVariantId:e,attachedImage:s=null}=t,{data:o}=await m.addItem({quantity:a,attachedImage:s,productVariantId:e});if(o instanceof n)throw o;return await this.fetch()}async removeItem(t){const{data:a}=await m.removeItem(t);if(a instanceof n)throw a;return await this.fetch()}async updateItem(t){const{data:a}=await m.updateItem(t);if(a instanceof n)throw a;return await this.fetch()}async note(t){const{data:a}=await m.note(t);if(a instanceof n)throw a}}const $=new O;class J{constructor(t,a){i(this,"data");i(this,"ok");i(this,"ERROR_MESSAGES",{INVALID_DATA:"The given data was invalid.",SKIP_SHIPPING:"cannot skip shipping step",SKIP_PAYMENT:"cannot skip payment step"});this.data=t,this.ok=a}onSuccess(t){return this.ok&&!(this.data instanceof n)&&t(this.data,()=>l("/checkout/thankyou")),this}onValidationErr(t){return this.data instanceof n&&this.data.detail===this.ERROR_MESSAGES.INVALID_DATA&&t(this.data),this}onSkipShippingStep(t){return this.data instanceof n&&this.data.detail===this.ERROR_MESSAGES.SKIP_SHIPPING&&t(this.data,()=>l("/checkout/shipping")),this}onSkipPaymentStep(t){return this.data instanceof n&&this.data.detail===this.ERROR_MESSAGES.SKIP_PAYMENT&&t(this.data,()=>l("/checkout/payment")),this}catch(t){return this.data instanceof n&&!this.ok&&t(this.data),this}finally(t){return t(),this}}class q{async placeExpressCheckoutOrder(t){const{data:a,ok:e}=await v.placeExpressCheckoutOrder(t);return new J(a,e)}async applyCoupon(t){const{data:a}=await v.applyCoupon(t);if(a instanceof n)throw a;return $.fetch()}async removeCoupons(){const{data:t}=await v.removeCoupons();if(t instanceof n)throw t;return $.fetch()}}i(q,"onSuccess");class Q{async fetch(){const{data:t}=await F.fetch();if(t instanceof n)throw t;return t}}class Y{async contact(t){const{data:a}=await L.contact(t);if(a instanceof n)throw a}}class z{fetchAll(t={}){const a=async e=>await N.fetchAll(e);return new b(a,t)}}class X{async answer(t){const{data:a}=await G.answer(t);if(a instanceof n)throw a;return a}}const W={BASE_URL:""};class I{constructor(){i(this,"product");i(this,"category");i(this,"cart");i(this,"checkout");i(this,"menu");i(this,"store");i(this,"page");i(this,"upsell");this.product=new H,this.category=new K,this.cart=new O,this.checkout=new q,this.menu=new Q,this.store=new Y,this.page=new z,this.upsell=new X}static init(){var o,u,A,f;if(typeof window.Dotshop>"u")throw new TypeError("Failed to initialize due to missing store context");const t=(o=window.Dotshop.store)==null?void 0:o.domain,a=(u=window.location)==null?void 0:u.hostname,s=[t!==a&&a!=="seller-area.youcan.shop"?(A=window.location)==null?void 0:A.origin:(f=window.Dotshop.store)==null?void 0:f.domain,W.BASE_URL];return g.init({csrfToken:window.Dotshop.csrfToken,baseUrl:d(s)}),new I}}return window.youcanjs=I.init(),I}); diff --git a/locales/ar.default.json b/locales/ar.default.json index 568cdd46..bf178c6d 100644 --- a/locales/ar.default.json +++ b/locales/ar.default.json @@ -25,7 +25,18 @@ "title": "مجموعات" }, "reviews": { - "ratings": "التقييمات" + "ratings": "التقييمات", + "upload_image": "تحميل الصورة (PNG، JPEG)", + "add_review": "أضف تعليقك", + "thankyou_review": "شكرا لك على تعليقك", + "review_apending": "سوف يتم مراجعة تعليقك من طرف إدارة الموقع", + "first_name_placeholder": "الاسم الأول", + "last_name_placeholder": "الاسم الأخير", + "email_placeholder": "البريد الإلكتروني", + "review_placeholder": "تعليقك", + "submit_button": "إرسال", + "review_submited": "تم إرسال تعليقك بنجاح", + "errors_message": "فشل في إرسال التعليق. الرجاء إعادة المحاولة." }, "cart": { "title": "عربة التسوق", diff --git a/locales/en.json b/locales/en.json index 8cab519d..f9f60f39 100644 --- a/locales/en.json +++ b/locales/en.json @@ -25,7 +25,18 @@ "title": "Collections" }, "reviews": { - "ratings": "Ratings" + "ratings": "Ratings", + "upload_image": "Upload image (PNG, JPEG)", + "add_review": "Add your comment.", + "thankyou_review": "Thank you for your review.", + "review_apending": "Your review is awaiting approval", + "first_name_placeholder": "First name", + "last_name_placeholder": "Last name", + "email_placeholder": "Email", + "review_placeholder": "Your review", + "submit_button": "Submit", + "review_submited": "Review submitted successfully", + "errors_message": "Failed to submit review. Please try again." }, "cart": { "title": "Shopping Cart", diff --git a/locales/fr.json b/locales/fr.json index 7c24e34e..b41bdbce 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -25,7 +25,18 @@ "title": "Collections" }, "reviews": { - "ratings": "Évaluations" + "ratings": "Évaluations", + "upload_image": "Télécharger une image (PNG, JPEG)", + "add_review": "Ajouter un commentaire", + "thankyou_review": "Merci pour votre commentaire", + "review_apending": "Votre commentaire est en attente de modération", + "first_name_placeholder": "Prénom", + "last_name_placeholder": "Nom", + "email_placeholder": "E-mail", + "review_placeholder": "Votre commentaire", + "submit_button": "Envoyer", + "review_submited": "Commentaire soumis avec succès", + "errors_message": "Échec de la soumission de l'avis. Veuillez réessayer." }, "cart": { "title": "Panier d'achat", diff --git a/snippets/reviews.liquid b/snippets/reviews.liquid index bd53c95d..d9063165 100644 --- a/snippets/reviews.liquid +++ b/snippets/reviews.liquid @@ -8,11 +8,68 @@ + + {% javascript %} const reviewsProductId = '{{ reviews_product_id }}'; + const REVIEWS_TRANSLATED_TEXT = { + successMessage: '{{ 'reviews.review_submited' | t }}', + errorMessage: "{{ 'reviews.errors_message' | t }}" + } document.addEventListener('DOMContentLoaded', () => { const parentContainer = '.yc-product-reviews'; diff --git a/styles/reviews.scss b/styles/reviews.scss index 5f593dbd..a2cf8215 100644 --- a/styles/reviews.scss +++ b/styles/reviews.scss @@ -41,6 +41,10 @@ flex-direction: column; gap: 14px; + @include max-screen('md') { + padding-bottom: 12px; + } + .header { display: flex; flex-direction: column; @@ -121,3 +125,317 @@ border: 1px solid #E6E6E6; } } + +.modal { + display: none; + justify-content: center; + align-items: center; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.55); + z-index: 1000; + + @include max-screen('md') { + padding: 12px; + } + + .modal-content { + background-color: #fff; + position: relative; + width: 627px; + + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-thumb { + background-color: #555; + border-radius: 4px; + } + + &::-webkit-scrollbar-track { + background-color: #f1f1f1; + border-radius: 5px; + } + + @include max-screen('md') { + max-height: 70vh; + overflow-y: scroll; + overflow-x: hidden; + } + + .modal-header { + display: flex; + position: sticky; + top: 0; + background-color: white; + padding: 0; + margin: 0; + padding: 32px; + + @include max-screen('md') { + padding: 12px; + } + } + + .modal-title { + font-size: 22px; + font-weight: 700; + } + + .thank-you-message { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + margin: 15px auto 55px; + gap: 16px; + + .icon { + width: 66px; + height: 66px; + } + + h2 { + font-size: 30px; + font-weight: 800; + } + + p { + font-weight: 400; + font-size: 16px; + } + } + } + + .close { + color: #d1c4c4; + font-size: 32px; + cursor: pointer; + background-color: #fff; + height: 40px; + width: 40px; + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + position: absolute; + left: 0; + display: flex; + justify-content: center; + align-items: center; + margin-top: -10px; + margin-left: -40px; + padding-bottom: 4px; + + [dir='ltr'] & { + right: 0; + left: unset; + margin-left: unset; + margin-right: -40px; + border-top-left-radius: unset; + border-bottom-left-radius: unset; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + } + + &:hover, + &:focus { + color: #000; + transition: all .2s ease-in-out; + } + + @include max-screen('md') { + margin: -10px auto 0 auto; + } + } +} + +#addReviewBtn { + margin: 40px auto 0 auto; +} + +#reviewForm { + display: flex; + flex-direction: column; + gap: 19px; + padding: 32px; + + @include max-screen('md') { + padding: 16px 12px; + } + + .inputs-wrapper { + display: flex; + gap: 10px; + + input { + width: 50%; + } + } + + input, textarea { + padding: 16px; + border-radius: 4px; + border: 1px solid #000; + background: #FFF; + box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.03); + resize: none; + outline: none; + font-family: var(--yc-font-family); + } + + input::placeholder { + color: #A8A8A8; + font-size: 13px; + font-weight: 400; + } + + .star-wrapper { + display: flex; + gap: 6px; + align-items: center; + } + + .review-stars-count { + font-size: 13px; + font-weight: 400; + color: #707070; + } + + + .star-rating { + display: flex; + font-size: 0; + position: relative; + flex-direction: row-reverse; + + input[type="radio"] { + display: none; + + &:checked ~ label, + + label:hover, + + label:hover ~ label { + color: var(--yc-reviews-stars-background); + } + } + + label { + color: #ccc; + cursor: pointer; + font-size: 24px; + margin: 0; + padding: 0 5px; + transition: color 0.2s; + + &:before { + content: "★"; + } + } + } + + .yc-btn { + margin-top: 5px; + } +} + +.yc-upload-preview .yc-image img { + max-width: 100%; + height: auto; +} + +.yc-upload-preview:first-child { + display: none; +} + + +.yc-image-preview-container { + display: flex; + flex-direction: row; + gap: 10px; + z-index: 2; + position: relative; +} + +.yc-image-preview img { + width: 60px; + height: 60px; + object-fit: cover; +} + +.yc-upload-wrapper .yc-image-preview:not(:first-child) { + width: 50px; + height: 50px; + margin-top: 10px; + position: relative; +} + +.yc-upload-wrapper .yc-image-preview:not(:first-child) img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.remove-button { + background-color: rgba(0, 0, 0, 0.5); + color: white; + border: none; + border-radius: 50%; + cursor: pointer; + width: 18px; + height: 18px; + margin-top: 3px; + display: flex; + align-items: center; + justify-content: center; +} + +.image-big-view { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.image-big-view img { + max-width: 80%; + max-height: 60vh; +} + +.yc-upload-preview .yc-image-preview:not(:first-child) { + width: 10px; + height: 10px; + position: relative; +} + +.uploaded-image { + width: 100%; + max-height: 120px; + object-fit: cover; +} + +.add-more { + margin-top: 8px; + font-size: 17px; + border: 1px solid #000; + width: 100%; + border-radius: 4px; +} + +.selected-image { + border: 2px solid #FDD07A; +} + +.field-error { + color: red; + font-size: 14px; + font-weight: 500; +}