nexo-solutions-review-carousel
Version:
A customizable review carousel component for React applications
179 lines (178 loc) • 9.94 kB
JavaScript
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
import React, { useState, useEffect } from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Pagination, Autoplay } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/pagination';
import { ReviewModal } from './ReviewModal';
import './ReviewCarousel.css';
// Google logo is imported in a way that works in both development and when published
var googleLogoPath = process.env.NODE_ENV === 'production'
? require('./assets/google-logo.svg').default
: './assets/google-logo.svg';
/**
* A customizable review carousel component
*/
export var ReviewCarousel = function (_a) {
var reviews = _a.reviews, language = _a.language, _b = _a.accentColor, accentColor = _b === void 0 ? '#ffc800' : _b, // Default accent color
_c = _a.sourceIconUrl, // Default accent color
sourceIconUrl = _c === void 0 ? googleLogoPath : _c, // Use included Google logo by default
onReviewClick = _a.onReviewClick, _d = _a.translations, translations = _d === void 0 ? {
readMore: {
en: 'Read More',
pt: 'Ler Mais',
},
swipeHint: {
en: 'Swipe to see more reviews',
pt: 'Deslize para ver mais avaliações',
},
viewOn: {
en: 'View on Google Maps',
pt: 'Ver no Google Maps',
},
} : _d;
var _e = useState({}), loadedImages = _e[0], setLoadedImages = _e[1];
var _f = useState(null), selectedReview = _f[0], setSelectedReview = _f[1];
var _g = useState(window.innerWidth <= 768), isMobile = _g[0], setIsMobile = _g[1];
// Handle resize to detect mobile
useEffect(function () {
var handleResize = function () {
setIsMobile(window.innerWidth <= 768);
};
window.addEventListener('resize', handleResize);
return function () { return window.removeEventListener('resize', handleResize); };
}, []);
// Preload images
useEffect(function () {
reviews.forEach(function (review) {
var img = new Image();
img.src = review.image;
img.onload = function () {
setLoadedImages(function (prev) {
var _a;
return (__assign(__assign({}, prev), (_a = {}, _a[review.image] = true, _a)));
});
};
img.onerror = function () {
// After 2 seconds, if image hasn't loaded, mark as failed
setTimeout(function () {
setLoadedImages(function (prev) {
var _a;
return (__assign(__assign({}, prev), (_a = {}, _a[review.image] = false, _a)));
});
}, 2000);
};
});
}, [reviews]);
var handleImageError = function (e, name) {
var target = e.target;
var nameParts = name.split(' ');
var initials = nameParts.length > 1
? "".concat(nameParts[0][0]).concat(nameParts[1][0])
: nameParts[0].substring(0, 2);
// Generate a deterministic color based on the name
var colors = ['#4285F4', '#DB4437', '#F4B400', '#0F9D58', '#9C27B0', '#FF6D00'];
var colorIndex = name.length % colors.length;
var backgroundColor = colors[colorIndex];
// Create a canvas element
var canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = 200;
var ctx = canvas.getContext('2d');
if (ctx) {
// Draw background circle
ctx.fillStyle = backgroundColor;
ctx.beginPath();
ctx.arc(100, 100, 100, 0, 2 * Math.PI);
ctx.fill();
// Add text
ctx.font = 'bold 80px Arial';
ctx.fillStyle = '#FFFFFF';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(initials.toUpperCase(), 100, 100);
// Replace the image source with the canvas data URL
target.src = canvas.toDataURL('image/png');
}
};
var handleReviewClick = function (review, e) {
if (onReviewClick) {
onReviewClick(review, e);
}
else {
// Default behavior: open modal
e.preventDefault();
e.stopPropagation();
setSelectedReview(review);
// Prevent scrolling when modal is open
document.body.style.overflow = 'hidden';
}
};
var closeReviewModal = function () {
setSelectedReview(null);
// Re-enable scrolling
document.body.style.overflow = 'auto';
};
var renderReviewCard = function (review, index) { return (React.createElement("div", { key: "review-".concat(index), className: "testimonial-card", onClick: function (e) { return handleReviewClick(review, e); }, role: "button", tabIndex: 0, onKeyPress: function (e) {
if (e.key === 'Enter' || e.key === ' ') {
handleReviewClick(review, e);
}
} },
React.createElement("div", { className: "testimonial-content" },
React.createElement("div", { className: "review-stars" },
__spreadArray([], Array(5), true).map(function (_, i) { return (React.createElement("i", { key: i, className: i < review.rating ? 'fas fa-star' : 'far fa-star' })); }),
review.sourceUrl && (React.createElement("a", { href: review.sourceUrl, target: "_blank", rel: "noopener noreferrer", className: "google-source-indicator", title: translations.viewOn[language] || translations.viewOn.en, onClick: function (e) { return e.stopPropagation(); } },
React.createElement("img", { src: sourceIconUrl, alt: "Source", className: "source-icon" })))),
React.createElement("div", { className: "testimonial-text-container" },
React.createElement("p", { className: "testimonial-text" }, review.comment[language] || review.comment.en),
React.createElement("button", { onClick: function (e) { return handleReviewClick(review, e); }, className: "read-more-btn", "aria-label": "Read full review" }, translations.readMore[language] || translations.readMore.en))),
React.createElement("div", { className: "testimonial-author" },
React.createElement("div", { className: "author-image" },
React.createElement("img", { src: review.image, alt: review.name, onError: function (e) { return handleImageError(e, review.name); } })),
React.createElement("div", { className: "author-info" },
React.createElement("h4", { className: "author-name" }, review.name),
React.createElement("span", { className: "author-location" }, review.location[language] || review.location.en))))); };
return (React.createElement("div", { className: "review-carousel-component" },
isMobile ? (React.createElement(React.Fragment, null,
React.createElement("div", { className: "testimonials-swiper" },
React.createElement(Swiper, { spaceBetween: 16, slidesPerView: 1.2, centeredSlides: true, pagination: {
clickable: true,
el: '.testimonials-pagination'
}, loop: true, autoplay: {
delay: 3000,
disableOnInteraction: false,
pauseOnMouseEnter: true
}, speed: 1000, modules: [Pagination, Autoplay] }, reviews.map(function (review, index) { return (React.createElement(SwiperSlide, { key: "review-slide-".concat(index) }, renderReviewCard(review, index))); }))),
React.createElement("div", { className: "testimonials-pagination" }),
React.createElement("div", { className: "swipe-hint text-center text-sm text-gray-500 mt-4" },
React.createElement("p", null,
React.createElement("i", { className: "fas fa-hand-point-right mr-2" }),
translations.swipeHint[language] || translations.swipeHint.en)))) : (React.createElement("div", { className: "testimonials-carousel" },
reviews.map(function (review, index) { return renderReviewCard(review, index); }),
reviews.map(function (review, index) { return renderReviewCard(review, "duplicate-".concat(index)); }))),
React.createElement(ReviewModal, { review: selectedReview, language: language, onClose: closeReviewModal, accentColor: accentColor, sourceIconUrl: sourceIconUrl, translations: {
viewOn: translations.viewOn
} }),
React.createElement("style", { dangerouslySetInnerHTML: {
__html: "\n .testimonials-pagination .swiper-pagination-bullet-active {\n background-color: ".concat(accentColor, ";\n }\n .review-stars, .review-stars i.fas.fa-star {\n color: ").concat(accentColor, ";\n }\n ")
} })));
};