UNPKG

nexo-solutions-review-carousel

Version:

A customizable review carousel component for React applications

179 lines (178 loc) 9.94 kB
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 ") } }))); };