UNPKG

@salla.sa/twilight-components

Version:
522 lines (521 loc) 21.4 kB
/*! * Crafted with ❤ by Salla */ import { h } from "@stencil/core"; import { CommentType } from "./interfaces"; import anime from "animejs"; import Helper from "../../Helpers/Helper"; import ChatBubbles from "../../assets/svg/chat-bubbles.svg"; export class SallaComments { constructor() { /** * Comment Type */ this.type = CommentType.PAGE; /** * Show or hide avatar */ this.showFormAvatar = false; /** * Hide Bought */ this.hideBought = false; /** * Determines if the comments are testimonials */ this.testimonials = false; // Translations this.noComments = salla.lang.get('blocks.comments.no_comments'); this.comment_title = salla.lang.get('blocks.comments.title'); this.comment_name = salla.lang.get('blocks.comments.comment'); this.showRatingSummary = salla.config.get('store.settings.rating.show_rating_summary'); this.allowLikes = salla.config.get('store.settings.rating.allow_likes'); salla.onReady(() => { this.allowLikes = salla.config.get('store.settings.rating.allow_likes'); this.showRatingSummary = salla.config.get('store.settings.rating.show_rating_summary'); }); salla.lang.onLoaded(() => { this.comment_title = salla.lang.get('blocks.comments.title'); this.comment_name = salla.lang.get('blocks.comments.comment'); this.noComments = salla.lang.get('pages.rating.no_ratings'); const setNestedAsync = (lang, key, value) => { return new Promise((resolve) => { salla.helpers.setNested(salla.lang.messages[lang], key, value); resolve(true); }); }; const setTranslations = async () => { await setNestedAsync('ar.trans', 'blocks.comments.most_helpful', 'الأكثر إفادة'); await setNestedAsync('en.trans', 'blocks.comments.most_helpful', 'Most helpful'); this.mostHelpfulLabel = salla.lang.get('blocks.comments.most_helpful'); this.comment_title = salla.lang.get('blocks.comments.title'); this.comment_name = salla.lang.get('blocks.comments.comment'); this.noComments = salla.lang.get('pages.rating.no_ratings'); }; setTranslations(); }); } // TOOD: it's a good idea to move this into lang.js // Pluralize a string based on the count pluralize(phrases, count) { const options = phrases.split('|'); const conditions = [ { condition: count === 0, index: 0 }, { condition: count === 1, index: 1 }, { condition: count === 2, index: 2 }, { condition: count > 2 && count <= 10, index: 3 }, { condition: count >= 11, index: 4 } ]; const { index } = conditions.find(({ condition }) => condition) || { index: options.length - 1 }; const selectedOption = options[index]; return selectedOption.replace(':count', salla.helpers.number(count.toString())) .replace(/\{[0-9]+\}/g, '') .replace(/\[\d+,\d+\]|\[11,\*\]/g, ''); } wrapConsoleError() { if (Salla.infiniteScroll.errorWrapped) { return; } (() => { const orig = console.error.bind(console); console.error = (...args) => { const msg = args[0]; // only rewrite the noisy one if (typeof msg === 'string' && msg.toLowerCase().replace(/\s/g, '').includes('infinitescroll')) { return console.log(...args); // downgrade to log } return orig(...args); // keep real errors }; })(); Salla.infiniteScroll.errorWrapped = true; } // Initiate infinite scroll initiateInfiniteScroll() { if (!this.wrapper) { console.error('Wrapper is undefined. Cannot initiate infinite scroll.'); return; } this.wrapConsoleError(); this.infiniteScroll = salla.infiniteScroll.initiate(this.wrapper, this.wrapper, { path: () => this.nextPage, history: false, nextPage: this.nextPage, scrollThreshold: false, }, true); this.infiniteScroll?.on('request', _response => { this.loading(); }); this.infiniteScroll?.on('load', response => { this.pagination = response.pagination; this.nextPage = typeof response.pagination.links === 'object' && !!response.pagination.links.next ? response.pagination.links.next : null; for (const card of this.handleResponse(response)) { this.wrapper.append(card); } const items = this.host.querySelectorAll('salla-comment-item:not(.animated):not(.s-comments-item-admin)'); this.animateItems(items); this.loading(false); }); this.infiniteScroll?.on('error', (e) => { salla.console.error('Error loading more comments:', e); }); } // Show/hide loading loading(isLoading = true) { const btnText = this.status?.querySelector('.s-button-text'); if (btnText) { Helper.toggleElementClassIf(btnText, 's-button-hide', 's-button-show', () => isLoading); this.btnLoader.style.display = isLoading ? 'inherit' : 'none'; } } // Animate newly added items animateItems(items) { anime({ targets: items, opacity: [0, 1], duration: 1200, translateY: [20, 0], delay: (_el, i) => i * 100, easing: 'easeOutExpo', complete: (_anim) => { for (const item of items) { item.classList.add('animated'); } } }); } /** * Reloads the comments data from the server */ async reload() { this.showPlaceholder = false; if (this.wrapper) { this.wrapper.innerHTML = ""; const loading = document.createElement('salla-loading'); this.wrapper.append(loading); } this.nextPage = null; this.loadInitialData(); } // Get comment item HTML getCommentHTML(comment) { const commentItem = document.createElement('salla-comment-item'); commentItem.comment = comment; commentItem.hideBought = this.hideBought; return commentItem; } // Parse response and return an array of comment items to be appended to the wrapper handleResponse(response) { return response.data?.map(comment => this.getCommentHTML(comment)) || []; } componentWillLoad() { return salla.onReady() .then(() => { this.showRatingSummary = salla.config.get('store.settings.rating.show_rating_summary'); }) .then(() => this.loading()) .then(() => { this.hideTitle = this.hideTitle || this.testimonials; this.hideForm = this.hideForm || this.testimonials; return this.loadInitialData(); }); } // Load initial data async loadInitialData() { try { let resp = { data: [], pagination: {} }; const searchParams = new URLSearchParams(window.location.search); if (searchParams.has('sort')) { this.sort = searchParams.get('sort'); } if (this.testimonials) { const params = { sort: this.sort, type: "store" }; resp = await salla.api.request('reviews', { params }, 'get'); } else { // Ensure sort is passed for regular comments as well resp = await salla.api.comment.getComments(this.type, this.itemId, 1, 5, this.sort); } if (!resp.data || !resp.data.length) { this.showPlaceholder = false; this.loading(false); return; } if (this.wrapper) { this.wrapper.innerHTML = ""; } this.comments = resp.data; this.pagination = resp.pagination; this.total = resp.pagination.total; this.nextPage = typeof resp.pagination.links === 'object' && !!resp.pagination.links.next ? resp.pagination.links.next : null; // Preserve sort param in next page URL for infinite scroll if (this.nextPage && this.sort) { try { const url = new URL(this.nextPage, window.location.origin); if (!url.searchParams.get('sort')) { url.searchParams.set('sort', this.sort); this.nextPage = url.toString(); } } catch (_e) { // fallback for relative next links const hasQuery = this.nextPage.includes('?'); const hasSort = /[?&]sort=/.test(this.nextPage); if (!hasSort) { this.nextPage = this.nextPage + (hasQuery ? '&' : '?') + `sort=${this.sort}`; } } } setTimeout(() => { for (const card of this.handleResponse(resp)) { this.wrapper.append(card); } this.initiateInfiniteScroll(); // Initiate infinite scroll after the initial data is loaded const items = this.wrapper.querySelectorAll('salla-comment-item:not(.animated)'); this.animateItems(items); }, 100); } catch (error) { console.error('Error loading initial data:', error); this.showPlaceholder = true; this.loading(false); } } // Get next page async loadMore() { this.infiniteScroll?.loadNextPage(); } render() { // We should show a different placeholder for pages and products (WIP) if (this.showPlaceholder) { return (h("div", null, !!this.total && !this.hideTitle ? h("h2", { class: "s-comments-title" }, this.blockTitle ? this.blockTitle : this.comment_title) : '', !this.hideForm && !this.testimonials ? h("salla-comment-form", { showAvatar: this.showFormAvatar, type: this.type, "item-id": this.itemId }) : '', h("div", { class: "s-comments-placeholder" }, h("span", { innerHTML: ChatBubbles }), h("p", null, this.noComments)))); } return (h("div", { class: `s-comments s-comments-${this.testimonials ? 'testimonials' : this.type}` }, h("div", { class: `${this.type === CommentType.PAGE ? "s-comments-page-container" : "s-comments-container"}` }, !!this.total && !this.hideTitle ? h("h2", { class: "s-comments-title" }, this.blockTitle ? this.blockTitle : this.comment_title) : '', !this.hideForm && h("salla-comment-form", { showAvatar: this.showFormAvatar, type: this.type, "item-id": this.itemId }), salla.url.is_page('product.single') ? h("salla-reviews-summary", { itemId: this.itemId }) : '', h("div", { class: `s-comments-header ${this.total ? "has-total" : ""}` }, !!this.total && h("span", { class: "s-comments-count-label", innerHTML: this.pluralize(this.comment_name, this.total) }), !!this.total && !this.testimonials && this.type !== CommentType.BLOG ? h("div", { class: "s-comments-filter-wrapper" }, h("label", { class: "s-comments-filter-label", htmlFor: "comments-filter" }, salla.lang.get('pages.categories.sorting')), h("select", { id: "comments-filter", "aria-label": salla.lang.get('pages.categories.sorting'), class: "s-form-control s-comments-sort-input", onChange: (e) => { this.sort = e.target.value; this.reload(); } }, h("option", { value: "latest", selected: true }, salla.lang.get("pages.testimonials.sort_by_date_desc")), h("option", { value: "oldest" }, salla.lang.get("pages.testimonials.sort_by_date_asc")), this.allowLikes && h("option", { value: "most_helpful" }, this.mostHelpfulLabel))) : ''), h("div", { ref: wrapper => { this.wrapper = wrapper; } }), this.nextPage && (h("div", { class: "s-infinite-scroll-wrapper", ref: status => { this.status = status; } }, h("button", { onClick: () => this.loadMore(), class: "s-infinite-scroll-btn s-button-btn s-button-primary", type: "button" }, h("span", { class: "s-button-text s-infinite-scroll-btn-text" }, this.loadMoreText ? this.loadMoreText : salla.lang.get('common.elements.load_more')), h("span", { class: "s-button-loader s-button-loader-center s-infinite-scroll-btn-loader", ref: btnLoader => { this.btnLoader = btnLoader; }, style: { "display": "none" } }))))))); } static get is() { return "salla-comments"; } static get originalStyleUrls() { return { "$": ["salla-comments.scss"] }; } static get styleUrls() { return { "$": ["salla-comments.css"] }; } static get properties() { return { "itemId": { "type": "number", "attribute": "item-id", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": true, "optional": false, "docs": { "tags": [], "text": "Page or product ID" }, "getter": false, "setter": false, "reflect": false }, "loadMoreText": { "type": "string", "attribute": "load-more-text", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Load more text" }, "getter": false, "setter": false, "reflect": false }, "hideForm": { "type": "boolean", "attribute": "hide-form", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Load more text" }, "getter": false, "setter": false, "reflect": false }, "blockTitle": { "type": "string", "attribute": "block-title", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Block Title" }, "getter": false, "setter": false, "reflect": false }, "hideTitle": { "type": "boolean", "attribute": "hide-title", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Hide Title" }, "getter": false, "setter": false, "reflect": false }, "type": { "type": "string", "attribute": "type", "mutable": false, "complexType": { "original": "CommentType.PAGE | CommentType.PRODUCT | CommentType.BLOG", "resolved": "CommentType.BLOG | CommentType.PAGE | CommentType.PRODUCT", "references": { "CommentType": { "location": "import", "path": "./interfaces", "id": "src/components/salla-comments/interfaces.ts::CommentType" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Comment Type" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "CommentType.PAGE" }, "showFormAvatar": { "type": "boolean", "attribute": "show-form-avatar", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Show or hide avatar" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "false" }, "hideBought": { "type": "boolean", "attribute": "hide-bought", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Hide Bought" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "false" }, "sort": { "type": "string", "attribute": "sort", "mutable": false, "complexType": { "original": "string | 'latest' | 'oldest' | 'bottom_rating' | 'top_rating'", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sort comments" }, "getter": false, "setter": false, "reflect": false }, "testimonials": { "type": "boolean", "attribute": "testimonials", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Determines if the comments are testimonials" }, "getter": false, "setter": false, "reflect": false, "defaultValue": "false" } }; } static get states() { return { "comments": {}, "pagination": {}, "total": {}, "showPlaceholder": {}, "nextPage": {}, "mostHelpfulLabel": {}, "noComments": {}, "comment_title": {}, "comment_name": {}, "placeholder_text": {}, "showRatingSummary": {}, "allowLikes": {} }; } static get methods() { return { "reload": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "Reloads the comments data from the server", "tags": [] } } }; } static get elementRef() { return "host"; } }