testimonial integration updated
This commit is contained in:
parent
75e3ec6764
commit
645c68fb5c
20
package-lock.json
generated
20
package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"sitemap": "^9.0.0",
|
"sitemap": "^9.0.0",
|
||||||
|
"swiper": "^12.0.3",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -9369,6 +9370,25 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swiper": {
|
||||||
|
"version": "12.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/swiper/-/swiper-12.0.3.tgz",
|
||||||
|
"integrity": "sha512-BHd6U1VPEIksrXlyXjMmRWO0onmdNPaTAFduzqR3pgjvi7KfmUCAm/0cj49u2D7B0zNjMw02TSeXfinC1hDCXg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/swiperjs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "open_collective",
|
||||||
|
"url": "http://opencollective.com/swiper"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tar-stream": {
|
"node_modules/tar-stream": {
|
||||||
"version": "1.6.2",
|
"version": "1.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz",
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
"react-google-recaptcha": "^3.1.0",
|
"react-google-recaptcha": "^3.1.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"sitemap": "^9.0.0",
|
"sitemap": "^9.0.0",
|
||||||
|
"swiper": "^12.0.3",
|
||||||
"xml2js": "^0.6.2"
|
"xml2js": "^0.6.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -5,12 +5,61 @@ import FAQ from "@/components/FAQ/FAQ";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import styles from "./about.module.css";
|
import styles from "./about.module.css";
|
||||||
import { featuresData, testimonialData, ctaData, aboutFaqData } from "@/utils/constant";
|
import { featuresData, ctaData, aboutFaqData } from "@/utils/constant";
|
||||||
|
import Testimonials from "@/components/Testimonials/Testimonials";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
import { Autoplay, Navigation } from 'swiper/modules';
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
import { FaStar, FaChevronLeft, FaChevronRight } from 'react-icons/fa';
|
||||||
|
|
||||||
|
interface Review {
|
||||||
|
text?: string;
|
||||||
|
description?: string;
|
||||||
|
snippet?: string;
|
||||||
|
review_text?: string;
|
||||||
|
body?: string;
|
||||||
|
content?: string;
|
||||||
|
rating: number;
|
||||||
|
profile_photo_url?: string;
|
||||||
|
author_profile_photo_url?: string;
|
||||||
|
user?: {
|
||||||
|
thumbnail?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
author_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function AboutContent() {
|
export default function AboutContent() {
|
||||||
const [currentTestimonial, setCurrentTestimonial] = useState(0);
|
const [reviews, setReviews] = useState<Review[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [expandedReview, setExpandedReview] = useState<number | null>(null);
|
||||||
|
const [swiperInstance, setSwiperInstance] = useState<any>(null);
|
||||||
|
|
||||||
|
// Auto-collapse expanded review after 10 seconds and handle autoplay
|
||||||
|
useEffect(() => {
|
||||||
|
if (expandedReview !== null) {
|
||||||
|
// Stop autoplay when a review is expanded
|
||||||
|
if (swiperInstance && swiperInstance.autoplay) {
|
||||||
|
swiperInstance.autoplay.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setExpandedReview(null);
|
||||||
|
}, 5000); // 10 seconds
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Resume autoplay when reviews are collapsed
|
||||||
|
if (swiperInstance && swiperInstance.autoplay) {
|
||||||
|
swiperInstance.autoplay.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [expandedReview, swiperInstance]);
|
||||||
|
|
||||||
// Animation variants
|
// Animation variants
|
||||||
const fadeInUp = {
|
const fadeInUp = {
|
||||||
@ -65,22 +114,58 @@ export default function AboutContent() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-slide testimonials
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
async function loadReviews() {
|
||||||
setCurrentTestimonial((prev) => (prev + 1) % testimonialData.length);
|
try {
|
||||||
}, 5000); // Change every 5 seconds
|
const res = await fetch("/api/reviews");
|
||||||
|
if (!res.ok) throw new Error("Failed to fetch");
|
||||||
return () => clearInterval(interval);
|
const data = await res.json();
|
||||||
|
const cleaned = (data.reviews || []).filter((r: Review) =>
|
||||||
|
(r.text || r.description || r.snippet || r.review_text || r.body || r.content) &&
|
||||||
|
r.rating >= 4
|
||||||
|
);
|
||||||
|
setReviews(cleaned);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("About: Failed to fetch reviews", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadReviews();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const nextTestimonial = () => {
|
const displayedReviews = reviews.length > 0 && reviews.length < 3
|
||||||
setCurrentTestimonial((prev) => (prev + 1) % testimonialData.length);
|
? [...reviews, ...reviews, ...reviews]
|
||||||
};
|
: reviews;
|
||||||
|
|
||||||
|
function renderStars(rating: number) {
|
||||||
|
return [...Array(5)].map((_, i) => (
|
||||||
|
<FaStar
|
||||||
|
key={i}
|
||||||
|
style={{ color: i < rating ? '#ffc107' : '#e4e5e9', fontSize: '1.2rem', marginRight: '2px' }}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getReviewText(r: Review) {
|
||||||
|
return r.text || r.description || r.snippet || r.review_text || r.body || r.content || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateText(text: string) {
|
||||||
|
return text.length > 200 ? text.substring(0, 200) + "..." : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfileImage(r: Review) {
|
||||||
|
const url = r.profile_photo_url || r.author_profile_photo_url || r.user?.thumbnail;
|
||||||
|
if (!url) return null;
|
||||||
|
return url.startsWith("http") ? url : `https://lh3.googleusercontent.com/${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitials(name: string) {
|
||||||
|
if (!name) return "U";
|
||||||
|
return name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
const prevTestimonial = () => {
|
|
||||||
setCurrentTestimonial((prev) => (prev - 1 + testimonialData.length) % testimonialData.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
@ -212,67 +297,92 @@ export default function AboutContent() {
|
|||||||
<span>ANTALYA</span>
|
<span>ANTALYA</span>
|
||||||
<Image src="/images/eat.png" alt="Testimonials Section Cutlery Icon" width={24} height={24} />
|
<Image src="/images/eat.png" alt="Testimonials Section Cutlery Icon" width={24} height={24} />
|
||||||
</div>
|
</div>
|
||||||
<motion.h2
|
<h2 className={styles.sectionTitleCenter}>What Our Guests Say</h2>
|
||||||
className={styles.sectionTitleCenter}
|
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={{ once: true }}
|
|
||||||
variants={fadeInUp}
|
|
||||||
>
|
|
||||||
What Our Guests Say
|
|
||||||
</motion.h2>
|
|
||||||
<motion.div
|
|
||||||
className={styles.testimonialSlider}
|
|
||||||
initial="hidden"
|
|
||||||
whileInView="visible"
|
|
||||||
viewport={{ once: true }}
|
|
||||||
variants={fadeIn}
|
|
||||||
>
|
|
||||||
<button className={styles.sliderBtn} onClick={prevTestimonial}>‹</button>
|
|
||||||
|
|
||||||
<AnimatePresence mode="wait">
|
<div className={styles.testimonialSlider}>
|
||||||
<motion.div
|
<button className={`${styles.sliderBtn} prev-btn`}>
|
||||||
key={currentTestimonial}
|
<FaChevronLeft />
|
||||||
className={styles.testimonialCard}
|
</button>
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
<div style={{ flex: 1, width: '100%' }}>
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
{loading ? (
|
||||||
transition={{ duration: 0.5 }}
|
<div className="text-center" style={{ color: '#F5E6D3', padding: '40px' , textAlign: 'center'}}>
|
||||||
>
|
<p>Loading reviews...</p>
|
||||||
<p className={styles.testimonialText}>"{testimonialData[currentTestimonial].text}"</p>
|
|
||||||
<div style={{ fontSize: '1.5rem', color: '#ffdf00', textAlign: 'center', letterSpacing: '3px' }}>
|
|
||||||
{'★★★★★'}
|
|
||||||
</div>
|
</div>
|
||||||
{/* <div className={styles.testimonialAuthor}>
|
) : (
|
||||||
<div className={styles.authorImageWrapper}>
|
<Swiper
|
||||||
<Image
|
spaceBetween={30}
|
||||||
src={testimonialData[currentTestimonial].image}
|
slidesPerView={1}
|
||||||
alt={testimonialData[currentTestimonial].name}
|
loop={true}
|
||||||
fill
|
navigation={{
|
||||||
className={styles.authorImage}
|
prevEl: '.prev-btn',
|
||||||
/>
|
nextEl: '.next-btn',
|
||||||
</div>
|
}}
|
||||||
<div className={styles.authorInfo}>
|
autoplay={{
|
||||||
<p className={styles.authorName}>{testimonialData[currentTestimonial].name}</p>
|
delay: 4000,
|
||||||
<p className={styles.authorRole}>{testimonialData[currentTestimonial].role}</p>
|
disableOnInteraction: false,
|
||||||
</div>
|
}}
|
||||||
</div> */}
|
onSwiper={setSwiperInstance}
|
||||||
</motion.div>
|
modules={[Autoplay, Navigation]}
|
||||||
</AnimatePresence>
|
className="testimonial_list"
|
||||||
|
style={{ paddingBottom: '30px' }}
|
||||||
|
>
|
||||||
|
{displayedReviews.map((r, index) => {
|
||||||
|
const fullText = getReviewText(r);
|
||||||
|
const isExpanded = expandedReview === index;
|
||||||
|
const profileImg = getProfileImage(r);
|
||||||
|
const name = r.user?.name || r.author_name || "Customer";
|
||||||
|
|
||||||
<button className={styles.sliderBtn} onClick={nextTestimonial}>›</button>
|
return (
|
||||||
|
<SwiperSlide key={index} style={{ height: 'auto' }}>
|
||||||
|
<div className={styles.testimonialCard}>
|
||||||
|
<div className={styles.testimonialAuthor}>
|
||||||
|
<div className={styles.authorImageWrapper}>
|
||||||
|
{profileImg ? (
|
||||||
|
<img
|
||||||
|
src={profileImg}
|
||||||
|
alt={name}
|
||||||
|
className={styles.authorImage}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
onError={(e) => ((e.target as HTMLImageElement).src = '/images/placeholder.png')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#333', fontSize: '24px', fontWeight: 'bold', color: '#fff' }}>
|
||||||
|
{getInitials(name)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.authorInfo}>
|
||||||
|
<p className={styles.authorName}>{name}</p>
|
||||||
|
<div className={styles.stars} style={{ justifyContent: 'start', marginBottom: 0 }}>
|
||||||
|
{renderStars(r.rating)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</motion.div>
|
<p className={styles.testimonialText}>
|
||||||
|
"{isExpanded ? fullText : truncateText(fullText)}"
|
||||||
|
</p>
|
||||||
|
|
||||||
{/* Slider dots */}
|
{fullText.length > 300 && (
|
||||||
<div className={styles.sliderDots}>
|
<button
|
||||||
{testimonialData.map((_, index) => (
|
style={{ background: 'none', border: 'none', color: '#a67c52', cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem', marginTop: '10px' }}
|
||||||
<button
|
onClick={() => setExpandedReview(isExpanded ? null : index)}
|
||||||
key={index}
|
>
|
||||||
className={`${styles.dot} ${index === currentTestimonial ? styles.activeDot : ''}`}
|
{isExpanded ? "Read Less" : "Read More"}
|
||||||
onClick={() => setCurrentTestimonial(index)}
|
</button>
|
||||||
/>
|
)}
|
||||||
))}
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Swiper>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className={`${styles.sliderBtn} next-btn`}>
|
||||||
|
<FaChevronRight />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
|
|||||||
@ -56,11 +56,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.testimonialsSection {
|
.testimonialsSection {
|
||||||
|
padding: 80px 20px;
|
||||||
background-color: #3a0c08;
|
background-color: #3a0c08;
|
||||||
background-image: url('/images/about-us/guest-bg.webp');
|
background-image: url('/images/about-us/guest-bg.webp');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-repeat: repeat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -149,6 +149,13 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stars {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
color: var(--color-paragraph);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* Features Section */
|
/* Features Section */
|
||||||
.featuresGrid {
|
.featuresGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -214,9 +221,9 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border: 2px solid #b07c4b;
|
border: 2px solid #b07c4b;
|
||||||
color: var(--color-alterparagraph) !important;
|
color: var(--color-alterparagraph) !important;
|
||||||
font-size: 2.5rem;
|
font-size: 1.2rem;
|
||||||
width: 50px;
|
width: 40px;
|
||||||
height: 50px;
|
height: 40px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -230,11 +237,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sliderBtn:first-child {
|
.sliderBtn:first-child {
|
||||||
left: 10px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sliderBtn:last-child {
|
.sliderBtn:last-child {
|
||||||
right: 10px;
|
right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sliderBtn:hover {
|
.sliderBtn:hover {
|
||||||
@ -245,21 +252,24 @@
|
|||||||
|
|
||||||
.testimonialCard {
|
.testimonialCard {
|
||||||
background-color: #F5E6D3;
|
background-color: #F5E6D3;
|
||||||
padding: 3rem 2.5rem;
|
padding: 2rem 2.5rem;
|
||||||
border: 2px solid #b07c4b;
|
border: 2px solid #b07c4b;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: 1.5rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 300px;
|
min-height: 400px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonialText {
|
.testimonialText {
|
||||||
font-family: var(--font-lato);
|
font-family: var(--font-lato);
|
||||||
font-size: 1.3rem;
|
font-size: 1.15rem;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
color: #a67c52;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonialAuthor {
|
.testimonialAuthor {
|
||||||
@ -292,9 +302,10 @@
|
|||||||
|
|
||||||
.authorName {
|
.authorName {
|
||||||
font-family: var(--font-playfair);
|
font-family: var(--font-playfair);
|
||||||
font-size: 1.2rem;
|
font-size: 1.1rem;
|
||||||
color: #3e2723;
|
color: #3e2723;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authorRole {
|
.authorRole {
|
||||||
@ -333,7 +344,7 @@
|
|||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 3rem;
|
/* margin-top: 3rem; */
|
||||||
padding: 0.8rem 2rem;
|
padding: 0.8rem 2rem;
|
||||||
border: 1px solid var(--color-gold);
|
border: 1px solid var(--color-gold);
|
||||||
color: #d3cab3;
|
color: #d3cab3;
|
||||||
|
|||||||
69
src/app/api/reviews/route.ts
Normal file
69
src/app/api/reviews/route.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
export const dynamic = 'force-static';
|
||||||
|
export const revalidate = 3600; // Cache for 1 hour during build/dev
|
||||||
|
|
||||||
|
interface SerpReview {
|
||||||
|
text?: string;
|
||||||
|
rating: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const apiKey = "37eb7f83988cfd76ffb5c5af9adc25652efe5607e39997fc7d0e054d690ef25e";
|
||||||
|
const placeId = "ChIJJU4PkTL1K4gRPSWJdlAQ7ko";
|
||||||
|
|
||||||
|
let allReviews: SerpReview[] = [];
|
||||||
|
let nextPageToken: string | null = null;
|
||||||
|
let pageCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (pageCount < 3) {
|
||||||
|
pageCount++;
|
||||||
|
const url: string = `https://serpapi.com/search.json?engine=google_maps_reviews&hl=en&api_key=${apiKey}&place_id=${placeId}${nextPageToken ? `&next_page_token=${nextPageToken}` : ""}`;
|
||||||
|
console.log(`API: Fetching Page ${pageCount} from URL: ${url.replace(apiKey, "HIDDEN")}`);
|
||||||
|
|
||||||
|
const response: Response = await fetch(url);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error(`API: SerpAPI fetch failed with status ${response.status}:`, errorText.slice(0, 200));
|
||||||
|
// If the first page fails, we should report it in the response
|
||||||
|
if (pageCount === 1) {
|
||||||
|
return NextResponse.json({ error: "SerpAPI Fetch Failed", status: response.status, details: errorText.slice(0, 200) }, { status: response.status });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: { reviews?: SerpReview[]; error?: string; serpapi_pagination?: { next_page_token?: string } } = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
console.error("API: SerpAPI JSON Error:", data.error);
|
||||||
|
if (pageCount === 1) {
|
||||||
|
return NextResponse.json({ error: "SerpAPI Error", details: data.error }, { status: 400 });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.reviews && data.reviews.length > 0) {
|
||||||
|
allReviews = [...allReviews, ...data.reviews];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.serpapi_pagination || !data.serpapi_pagination.next_page_token) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPageToken = data.serpapi_pagination.next_page_token;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ reviews: allReviews, total: allReviews.length });
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
const stack = error instanceof Error ? error.stack : "";
|
||||||
|
console.error("API: Unexpected Server Error:", message, stack);
|
||||||
|
return NextResponse.json({ error: "Failed to fetch reviews", details: message, stack: stack?.slice(0, 200) }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -291,3 +291,116 @@ h2 {
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.google-review-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
height: 100%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-review-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-avatar {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #546e7a;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-avatar img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-user-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-stars {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-stars span {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #ffc107;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-review-images {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.google-review-photo {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-more-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #d32f2f;
|
||||||
|
padding: 15px 0 0 0;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-more-btn:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equal-height {
|
||||||
|
min-height: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Home specific adjustments to maintain original layout density */
|
||||||
|
.google-review-card-home {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
@ -226,7 +226,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 500px) {
|
||||||
.sliderContainer {
|
.sliderContainer {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -407,8 +407,8 @@
|
|||||||
margin-left: 10%;
|
margin-left: 10%;
|
||||||
right: auto;
|
right: auto;
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
border-top: 10px solid #000000;
|
border-top: 10px solid #c49c5c;
|
||||||
border-left: 10px solid #000000;
|
border-left: 10px solid #c49c5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.callWidgetLeft {
|
.callWidgetLeft {
|
||||||
|
|||||||
@ -39,6 +39,7 @@
|
|||||||
.sliderContainer {
|
.sliderContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
max-width: 1400px;
|
max-width: 1400px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -57,22 +58,16 @@
|
|||||||
background: #242323;
|
background: #242323;
|
||||||
border: 14px solid rgb(196, 156, 92);
|
border: 14px solid rgb(196, 156, 92);
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
padding: 40px 30px;
|
padding: 30px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
/* Flex basis for 3 items: 100% / 3 = 33.333% */
|
|
||||||
flex: 0 0 33.333%;
|
|
||||||
max-width: 33.333%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0 15px;
|
height: 100%;
|
||||||
/* Visual gap */
|
width: 100%;
|
||||||
/* Adjust flex basis to account for margin: calc(33.333% - 30px) */
|
|
||||||
flex: 0 0 calc(33.333% - 30px);
|
|
||||||
max-width: calc(33.333% - 30px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatarContainer {
|
.avatarContainer {
|
||||||
@ -83,7 +78,9 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
background: rgb(196, 156, 92);
|
background: rgb(196, 156, 92);
|
||||||
/* Gold gradient border effect */
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@ -103,58 +100,93 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar,
|
||||||
.authorImage {
|
.authorImage {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
background-color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
font-family: var(--font-inter), sans-serif;
|
font-family: var(--font-inter), sans-serif;
|
||||||
/* Assuming Inter or similar */
|
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
color: #f5e6d3;
|
color: #f5e6d3;
|
||||||
margin-bottom: 30px;
|
/* White as seen in image */
|
||||||
font-weight: 500;
|
margin-bottom: 5px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
font-family: var(--font-playfair);
|
font-family: var(--font-paragraph);
|
||||||
font-size: 1.2rem;
|
color: var(--color-paragraph);
|
||||||
line-height: 1.6;
|
font-size: 1.15rem;
|
||||||
color: #e0e0e0;
|
line-height: 1.7;
|
||||||
margin-bottom: 30px;
|
/* White as seen in image */
|
||||||
|
margin-bottom: 20px;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
opacity: 0.95;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initials {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #333;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readMoreBtn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #f5e6d3;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: 10px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-family: var(--font-inter);
|
||||||
|
}
|
||||||
|
|
||||||
|
.readMoreBtn:hover {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stars {
|
.stars {
|
||||||
color: var(--color-paragraph);
|
color: var(--color-paragraph);
|
||||||
font-size: 1.5rem;
|
font-size: 16px;
|
||||||
letter-spacing: 5px;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3px;
|
||||||
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.navBtn {
|
||||||
position: absolute;
|
background: transparent;
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
color: var(--color-paragraph);
|
|
||||||
font-size: 3rem;
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
border: none;
|
||||||
transition: transform 0.2s;
|
color: rgb(196, 156, 92);
|
||||||
|
font-size: 2.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow:hover {
|
.navBtn:hover {
|
||||||
transform: translateY(-50%) scale(1.1);
|
transform: scale(1.2);
|
||||||
}
|
color: #fff;
|
||||||
|
|
||||||
.prevArrow {
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nextArrow {
|
|
||||||
right: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lanterns decoration placeholder */
|
/* Lanterns decoration placeholder */
|
||||||
@ -191,40 +223,44 @@
|
|||||||
.sliderContainer {
|
.sliderContainer {
|
||||||
padding: 0 50px;
|
padding: 0 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
|
||||||
font-size: 2.8rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
@media (max-width: 1200px) {
|
||||||
.sliderContainer {
|
.sliderContainer {
|
||||||
padding: 0 40px;
|
padding: 0 40px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.arrow {
|
@media (min-width: 1025px) {
|
||||||
font-size: 2.5rem;
|
.navBtn {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prevArrow {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextArrow {
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.sliderContainer {
|
.sliderContainer {
|
||||||
padding: 0 30px;
|
padding: 0 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.title {
|
||||||
flex: 0 0 100%;
|
font-size: 2.5rem;
|
||||||
max-width: 50%;
|
margin-bottom: 40px;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:nth-child(3) {
|
.navBtn {
|
||||||
display: none;
|
position: absolute;
|
||||||
}
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
.arrow {
|
|
||||||
font-size: 2.2rem;
|
font-size: 2.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,20 +271,30 @@
|
|||||||
.nextArrow {
|
.nextArrow {
|
||||||
right: 5px;
|
right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sliderContainer {
|
.sliderContainer {
|
||||||
padding: 0 20px;
|
padding: 0 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow {
|
.card {
|
||||||
font-size: 2rem;
|
border-width: 10px;
|
||||||
|
/* Strong but not too thick for small screens */
|
||||||
|
padding: 25px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatarContainer {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navBtn {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prevArrow {
|
.prevArrow {
|
||||||
@ -262,28 +308,23 @@
|
|||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.sliderContainer {
|
.sliderContainer {
|
||||||
padding: 0 15px;
|
padding: 0 30px;
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
flex: 0 0 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:nth-child(2) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:nth-child(3) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 2rem;
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 60px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-width: 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navBtn {
|
||||||
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,41 +3,154 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import styles from './Testimonials.module.css'
|
import styles from './Testimonials.module.css'
|
||||||
import { testimonialData } from '@/utils/constant'
|
import { Swiper, SwiperSlide } from 'swiper/react'
|
||||||
|
import { Autoplay, Navigation } from 'swiper/modules'
|
||||||
|
import 'swiper/css'
|
||||||
|
import 'swiper/css/navigation'
|
||||||
|
import { FaStar, FaChevronLeft, FaChevronRight } from 'react-icons/fa'
|
||||||
|
|
||||||
const testimonials = testimonialData
|
|
||||||
|
|
||||||
|
|
||||||
|
interface Review {
|
||||||
|
text?: string;
|
||||||
|
description?: string;
|
||||||
|
snippet?: string;
|
||||||
|
review_text?: string;
|
||||||
|
body?: string;
|
||||||
|
content?: string;
|
||||||
|
rating: number;
|
||||||
|
profile_photo_url?: string;
|
||||||
|
author_profile_photo_url?: string;
|
||||||
|
user?: {
|
||||||
|
thumbnail?: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
author_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Testimonials() {
|
export default function Testimonials() {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0)
|
const [reviews, setReviews] = useState<Review[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [expandedReview, setExpandedReview] = useState<number | null>(null);
|
||||||
|
const [swiperInstance, setSwiperInstance] = useState<any>(null);
|
||||||
|
|
||||||
const nextSlide = () => {
|
const testimonial_list_slider: any = {
|
||||||
setCurrentIndex((prev) => (prev + 1) % (testimonials.length - 2))
|
spaceBetween: 30,
|
||||||
}
|
slidesPerView: 3,
|
||||||
|
navigation: {
|
||||||
const prevSlide = () => {
|
prevEl: `.${styles.prevArrow}`,
|
||||||
setCurrentIndex((prev) => (prev === 0 ? testimonials.length - 3 : prev - 1))
|
nextEl: `.${styles.nextArrow}`,
|
||||||
}
|
},
|
||||||
|
pagination: false,
|
||||||
// Auto-slide effect
|
loop: true,
|
||||||
useEffect(() => {
|
autoplay: {
|
||||||
const interval = setInterval(() => {
|
delay: 3000,
|
||||||
nextSlide()
|
disableOnInteraction: false,
|
||||||
}, 5000) // Change slide every 5 seconds
|
},
|
||||||
|
modules: [Autoplay, Navigation],
|
||||||
return () => clearInterval(interval)
|
breakpoints: {
|
||||||
}, [])
|
0: {
|
||||||
|
slidesPerView: 1,
|
||||||
// Get the 3 visible testimonials
|
},
|
||||||
const getVisibleTestimonials = () => {
|
768: {
|
||||||
const items = []
|
slidesPerView: 2,
|
||||||
for (let i = 0; i < 3; i++) {
|
},
|
||||||
const index = (currentIndex + i) % testimonials.length
|
1024: {
|
||||||
items.push(testimonials[index])
|
slidesPerView: 3,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return items
|
};
|
||||||
|
|
||||||
|
// Auto-collapse expanded review after 10 seconds and handle autoplay
|
||||||
|
useEffect(() => {
|
||||||
|
if (expandedReview !== null) {
|
||||||
|
// Stop autoplay when a review is expanded
|
||||||
|
if (swiperInstance && swiperInstance.autoplay) {
|
||||||
|
swiperInstance.autoplay.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setExpandedReview(null);
|
||||||
|
}, 10000); // 10 seconds
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Resume autoplay when reviews are collapsed
|
||||||
|
if (swiperInstance && swiperInstance.autoplay) {
|
||||||
|
swiperInstance.autoplay.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [expandedReview, swiperInstance]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadReviews() {
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/reviews");
|
||||||
|
if (!res.ok) {
|
||||||
|
let details = "Unknown error";
|
||||||
|
try {
|
||||||
|
const errorData = await res.json();
|
||||||
|
details = errorData.details || errorData.error || "No details";
|
||||||
|
} catch (e) { }
|
||||||
|
throw new Error(`HTTP error! status: ${res.status} - ${details}`);
|
||||||
|
}
|
||||||
|
const text = await res.text();
|
||||||
|
let dataAt;
|
||||||
|
try {
|
||||||
|
dataAt = JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Home: Invalid JSON response", text.slice(0, 100));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = (dataAt.reviews || []).filter((r: Review) =>
|
||||||
|
(r.text || r.description || r.snippet || r.review_text || r.body || r.content) &&
|
||||||
|
r.rating >= 4
|
||||||
|
);
|
||||||
|
setReviews(cleaned);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Home: Failed to fetch reviews", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadReviews();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const displayedReviews = reviews.length > 0 && reviews.length < 3
|
||||||
|
? [...reviews, ...reviews, ...reviews]
|
||||||
|
: reviews;
|
||||||
|
|
||||||
|
function renderStars(rating: number) {
|
||||||
|
return [...Array(5)].map((_, i) => (
|
||||||
|
<FaStar
|
||||||
|
key={i}
|
||||||
|
style={{ color: i < rating ? '#ffc107' : '#e4e5e9', fontSize: '16px', marginRight: '5px' }}
|
||||||
|
/>
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleItems = getVisibleTestimonials()
|
function getReviewText(r: Review) {
|
||||||
|
return r.text || r.description || r.snippet || r.review_text || r.body || r.content || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateText(text: string) {
|
||||||
|
return text.length > 150 ? text.substring(0, 150) + "..." : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfileImage(r: Review) {
|
||||||
|
const url = r.profile_photo_url || r.author_profile_photo_url || r.user?.thumbnail;
|
||||||
|
if (!url) return null;
|
||||||
|
return url.startsWith("http") ? url : `https://lh3.googleusercontent.com/${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitials(name: string) {
|
||||||
|
if (!name) return "U";
|
||||||
|
return name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
@ -49,31 +162,76 @@ export default function Testimonials() {
|
|||||||
<h2 className={styles.title}>Testimonials</h2>
|
<h2 className={styles.title}>Testimonials</h2>
|
||||||
|
|
||||||
<div className={styles.sliderContainer}>
|
<div className={styles.sliderContainer}>
|
||||||
<button className={`${styles.arrow} ${styles.prevArrow}`} onClick={prevSlide} suppressHydrationWarning>←</button>
|
{loading ? (
|
||||||
|
<div className="text-center" style={{ color: 'var(--color-paragraph)', padding: '40px' , textAlign: 'center'}}>
|
||||||
{visibleItems.map((item) => (
|
<p>Loading reviews...</p>
|
||||||
<div key={item.id} className={styles.card}>
|
|
||||||
<div className={styles.avatarContainer}>
|
|
||||||
<div className={styles.avatar} style={{ overflow: 'hidden' }}>
|
|
||||||
<div className={styles.authorImageWrapper}>
|
|
||||||
<Image
|
|
||||||
src={item.image}
|
|
||||||
alt={`${item.name} - Review ${item.id}`}
|
|
||||||
fill
|
|
||||||
className={styles.authorImage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <h3 className={styles.name}>— {item.name}</h3> */}
|
|
||||||
<p className={styles.testimonialText}>"{item.text}"</p>
|
|
||||||
<div style={{ fontSize: '1.5rem', color: '#ffdf00', textAlign: 'center', letterSpacing: '3px', marginTop: '20px' }}>
|
|
||||||
{'★★★★★'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
|
<>
|
||||||
|
<button className={`${styles.navBtn} ${styles.prevArrow}`}>
|
||||||
|
<FaChevronLeft />
|
||||||
|
</button>
|
||||||
|
<Swiper
|
||||||
|
{...testimonial_list_slider}
|
||||||
|
onSwiper={setSwiperInstance}
|
||||||
|
className="testimonial_list"
|
||||||
|
style={{ paddingBottom: '30px', flex: 1 }}
|
||||||
|
>
|
||||||
|
{displayedReviews.map((r, index) => {
|
||||||
|
const fullText = getReviewText(r);
|
||||||
|
const isExpanded = expandedReview === index;
|
||||||
|
const profileImg = getProfileImage(r);
|
||||||
|
const name = r.user?.name || r.author_name || "Customer";
|
||||||
|
|
||||||
<button className={`${styles.arrow} ${styles.nextArrow}`} onClick={nextSlide} suppressHydrationWarning>→</button>
|
return (
|
||||||
|
<SwiperSlide key={index} style={{ height: 'auto' }}>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.avatarContainer}>
|
||||||
|
<div className={styles.authorImageWrapper}>
|
||||||
|
{profileImg ? (
|
||||||
|
<img
|
||||||
|
src={profileImg}
|
||||||
|
alt={name}
|
||||||
|
className={styles.authorImage}
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
onError={(e) => ((e.target as HTMLImageElement).src = '/images/placeholder.png')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className={styles.initials}>
|
||||||
|
{getInitials(name)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={styles.name}>{name}</p>
|
||||||
|
|
||||||
|
<div className={styles.stars}>
|
||||||
|
{renderStars(r.rating)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={styles.text}>
|
||||||
|
"{isExpanded ? fullText : truncateText(fullText)}"
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={styles.readMoreBtn}
|
||||||
|
onClick={() => setExpandedReview(isExpanded ? null : index)}
|
||||||
|
>
|
||||||
|
{isExpanded ? "Read Less" : "Read More"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Swiper>
|
||||||
|
<button className={`${styles.navBtn} ${styles.nextArrow}`}>
|
||||||
|
<FaChevronRight />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user