@salla.sa/twilight-components
Version:
Salla Web Component
522 lines (521 loc) • 21.4 kB
JavaScript
/*!
* 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"; }
}