228 lines
13 KiB
TypeScript
228 lines
13 KiB
TypeScript
"use client";
|
|
import React, { useState, useRef, useEffect } from 'react';
|
|
import Slider from 'react-slick';
|
|
import Link from 'next/link';
|
|
import GsapReveal from '@/components/common/GsapReveal';
|
|
import GoogleReviewsBranding from '@/components/common/GoogleReviewsBranding';
|
|
|
|
const Testimonial = () => {
|
|
const [reviews, setReviews] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const sliderRef = useRef<Slider>(null);
|
|
|
|
useEffect(() => {
|
|
async function loadReviews() {
|
|
try {
|
|
const res = await fetch("/api/reviews");
|
|
const data = await res.json();
|
|
// Filter for reviews with text (showing ALL reviews as requested)
|
|
const cleaned = (data.reviews || []).filter((r: any) =>
|
|
(r.snippet || r.text || r.review_text || r.description || r.body || r.content)
|
|
);
|
|
setReviews(cleaned);
|
|
} catch (error) {
|
|
console.error("Failed to fetch reviews", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
loadReviews();
|
|
}, []);
|
|
|
|
// If reviews are sparse, duplicate them to ensure infinite slider works perfectly
|
|
const displayedReviews = reviews.length > 0 && reviews.length < 3
|
|
? [...reviews, ...reviews, ...reviews]
|
|
: reviews;
|
|
|
|
const settings = {
|
|
dots: false,
|
|
arrows: false,
|
|
infinite: displayedReviews.length > 1,
|
|
speed: 600,
|
|
slidesToShow: 1,
|
|
slidesToScroll: 1,
|
|
autoplay: true,
|
|
autoplaySpeed: 5000,
|
|
responsive: [
|
|
{
|
|
breakpoint: 1024,
|
|
settings: {
|
|
slidesToShow: 1,
|
|
}
|
|
}
|
|
]
|
|
};
|
|
|
|
function getReviewText(r: any) {
|
|
return r.text || r.description || r.snippet || r.review_text || r.body || r.content || "";
|
|
}
|
|
|
|
function getProfileImage(r: any) {
|
|
const url = r.profile_photo_url || r.author_profile_photo_url || r.user?.thumbnail || r.user?.profile_photo_url || r.thumbnail || r.profile_picture || r.avatar || r.author_image;
|
|
if (!url) return null;
|
|
if (url.startsWith("http")) return url;
|
|
return `https://lh3.googleusercontent.com/${url}`;
|
|
}
|
|
|
|
function getInitials(name: string) {
|
|
if (!name) return "U";
|
|
return name.split(' ').map(n => n.charAt(0)).join('').substring(0, 2).toUpperCase();
|
|
}
|
|
|
|
return (
|
|
<div className="testimonial1-section-area sp1" style={{
|
|
backgroundImage: 'url(/assets/img/home/section8/bg.webp)',
|
|
backgroundPosition: 'center',
|
|
backgroundRepeat: 'no-repeat',
|
|
backgroundSize: 'cover',
|
|
backgroundAttachment: 'fixed',
|
|
position: 'relative',
|
|
// paddingBottom: '80px'
|
|
}}>
|
|
<div className="section-overlay"></div>
|
|
<div className="container" style={{ position: 'relative', zIndex: 1 }}>
|
|
{/* Section Header */}
|
|
<div className="row">
|
|
<div className="col-lg-7 m-auto">
|
|
<div className="testimonial-header text-center heading2 space-margin60">
|
|
<GsapReveal y={20}>
|
|
<h5><span><img src="/assets/img/fav-icon.webp" alt="" style={{ width: '20px', height: '20px', objectFit: 'contain' }} /></span>Testimonials</h5>
|
|
</GsapReveal>
|
|
<div className="space20"></div>
|
|
<GsapReveal y={20} delay={0.2}>
|
|
<h2 className="text-anime-style-3 text-white">What Our Clients Say</h2>
|
|
</GsapReveal>
|
|
<div className="space20"></div>
|
|
<GsapReveal y={10} delay={0.3}>
|
|
<div className="d-flex justify-content-center">
|
|
<GoogleReviewsBranding />
|
|
</div>
|
|
</GsapReveal>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content Row */}
|
|
<div className="row align-items-center g-5">
|
|
{/* Left: Single Primary Image */}
|
|
<div className="col-lg-5">
|
|
<GsapReveal y={30} duration={1}>
|
|
<div className="testimonial-img-single" style={{ borderRadius: '24px', overflow: 'hidden' }}>
|
|
<img src="/assets/img/home/section8/left.webp" alt="Testimonials" />
|
|
</div>
|
|
</GsapReveal>
|
|
</div>
|
|
|
|
{/* Right: Slider */}
|
|
<div className="col-lg-7">
|
|
<div className="testi-slider-container" style={{ paddingLeft: '20px' }}>
|
|
{loading ? (
|
|
<div className="text-center py-5">
|
|
<div className="spinner-border text-primary" role="status">
|
|
<span className="visually-hidden">Loading reviews...</span>
|
|
</div>
|
|
<p className="mt-3 text-white">Fetching live Google reviews...</p>
|
|
</div>
|
|
) : displayedReviews.length > 0 ? (
|
|
<div className="testi-slider-wrapper" style={{ position: 'relative' }}>
|
|
|
|
<Slider ref={sliderRef} {...settings}>
|
|
{displayedReviews.map((slide, index) => {
|
|
const profileImg = getProfileImage(slide);
|
|
const name = slide.user?.name || slide.author_name || "Valued Client";
|
|
return (
|
|
<div key={index} className="testi-card-wrapper" style={{ outline: 'none', padding: '10px' }}>
|
|
<div className="testimonial-bubble" style={{
|
|
background: '#1a1f2b',
|
|
borderRadius: '24px',
|
|
padding: '40px',
|
|
position: 'relative',
|
|
transition: 'transform 0.3s ease'
|
|
}}>
|
|
<img src="/assets/img/icons/quito1.svg" alt="" style={{ position: 'absolute', right: 30, top: 30, opacity: 0.15, width: 50 }} />
|
|
|
|
{/* Stars */}
|
|
<div className="stars" style={{ display: 'flex', gap: '6px', marginBottom: '25px' }}>
|
|
{[...Array(slide.rating || 5)].map((_, i) => (
|
|
<i key={i} className="fa-solid fa-star" style={{ color: '#FBBC04' }}></i>
|
|
))}
|
|
</div>
|
|
|
|
<p style={{
|
|
color: 'rgba(255,255,255,0.95)',
|
|
lineHeight: '1.8',
|
|
fontWeight: 400,
|
|
margin: 0,
|
|
fontStyle: 'italic',
|
|
overflow: 'hidden',
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 4,
|
|
WebkitBoxOrient: 'vertical',
|
|
textShadow: '0 2px 4px rgba(0,0,0,0.2)'
|
|
}}>
|
|
"{getReviewText(slide)}"
|
|
</p>
|
|
</div>
|
|
|
|
<div className="testi-author-info" style={{ display: 'flex', alignItems: 'center', marginTop: '35px', paddingLeft: '30px' }}>
|
|
<div style={{
|
|
width: 75,
|
|
height: 75,
|
|
borderRadius: '50%',
|
|
overflow: 'hidden',
|
|
border: '4px solid #3779b9',
|
|
boxShadow: '0 15px 30px rgba(0,0,0,0.2)',
|
|
background: '#252b3a',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flexShrink: 0
|
|
}}>
|
|
{profileImg ? (
|
|
<img src={profileImg} alt={name} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
|
) : (
|
|
<span style={{ fontWeight: 'bold', color: '#fff' }}>{getInitials(name)}</span>
|
|
)}
|
|
</div>
|
|
<div style={{ marginLeft: '25px' }}>
|
|
<h4 style={{ margin: 0, fontWeight: 700, color: '#fff', letterSpacing: '0.5px' }}>{name}</h4>
|
|
{/* <div style={{ display: 'flex', alignItems: 'center', marginTop: '5px' }}>
|
|
<i className="fa-brands fa-google" style={{ color: '#3779b9', marginRight: '8px' }}></i>
|
|
<p style={{ margin: 0, color: 'rgba(255,255,255,0.5)', fontWeight: 500 }}>Verified Google Reviewer</p>
|
|
</div> */}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</Slider>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-5">
|
|
<p className="text-white">Professional services you can trust.</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Google Action Button */}
|
|
<div className="mt-5 text-center text-lg-start" style={{ paddingLeft: '20px' }}>
|
|
<GsapReveal y={20} delay={0.5}>
|
|
<Link
|
|
href="https://www.google.com/maps/place/Metatron+Cube+Software+Solutions/@34.0518468,-56.3267266,3z/data=!4m8!3m7!1s0x89d4d4dc0e01490b:0xf0ca6a97298a109c!8m2!3d34.0518468!4d-56.3267266!9m1!1b1!16s%2Fg%2F11k197xnvf?entry=ttu&g_ep=EgoyMDI1MTAxMy4wIKXMDSoASAFQAw%3D%3D"
|
|
target="_blank"
|
|
className="vl-btn2"
|
|
style={{ display: 'inline-flex', alignItems: 'center', gap: '10px' }}
|
|
>
|
|
Review us on Google <i className="fa-brands fa-google"></i>
|
|
</Link>
|
|
</GsapReveal>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Testimonial;
|