252 lines
10 KiB
TypeScript
252 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import Image from 'next/image'
|
|
import styles from './Testimonials.module.css'
|
|
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 Testimonials() {
|
|
const [reviews, setReviews] = useState<Review[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [expandedReview, setExpandedReview] = useState<number | null>(null);
|
|
const [swiperInstance, setSwiperInstance] = useState<any>(null);
|
|
|
|
const testimonial_list_slider: any = {
|
|
spaceBetween: 30,
|
|
slidesPerView: 3,
|
|
navigation: {
|
|
prevEl: `.${styles.prevArrow}`,
|
|
nextEl: `.${styles.nextArrow}`,
|
|
},
|
|
pagination: false,
|
|
loop: true,
|
|
autoplay: {
|
|
delay: 3000,
|
|
disableOnInteraction: false,
|
|
},
|
|
modules: [Autoplay, Navigation],
|
|
breakpoints: {
|
|
0: {
|
|
slidesPerView: 1,
|
|
},
|
|
768: {
|
|
slidesPerView: 2,
|
|
},
|
|
1024: {
|
|
slidesPerView: 3,
|
|
},
|
|
}
|
|
};
|
|
|
|
// 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) { }
|
|
console.error(`HTTP error! status: ${res.status} - ${details}`);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
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' }}
|
|
/>
|
|
));
|
|
}
|
|
|
|
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 (
|
|
<section className={styles.section}>
|
|
<div className={styles.smallHeading} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '10px' }}>
|
|
<Image src="/images/dinner.png" alt="Antalya Dinner Icon" width={24} height={24} />
|
|
<span>ANTALYA</span>
|
|
<Image src="/images/eat.png" alt="Antalya Cutlery Icon" width={24} height={24} />
|
|
</div>
|
|
<h2 className={styles.title}>Testimonials</h2>
|
|
|
|
<div className={styles.sliderContainer}>
|
|
{loading ? (
|
|
<div className="text-center" style={{ color: 'var(--color-paragraph)', padding: '40px' , textAlign: 'center'}}>
|
|
<p>Loading reviews...</p>
|
|
</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";
|
|
|
|
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 className={styles.buttonContainer}>
|
|
<a
|
|
href="https://www.google.com/search?sca_esv=fb3147f266a54277&sxsrf=AE3TifPK9lVB_UK5Mt9ko4Ht65w63RgqUQ:1764431326889&si=AMgyJEtREmoPL4P1I5IDCfuA8gybfVI2d5Uj7QMwYCZHKDZ-E3A-qenJpQm2J1V3Wa6_UKYzIQhT0idJrAopWIgZ0RiDDHrNP0RtPpfTzWgU2637exPDNkEZu0WuVN4TEdgoYqqtQwcWsOUIFi4JvvmrZPyybRzrcg%3D%3D&q=Antalya+Turkish+Restaurant+Reviews&sa=X&ved=2ahUKEwic4uDz2peRAxUbTmwGHWwWBRoQ0bkNegQIJRAE&biw=1528&bih=786&dpr=1.25&zx=1764431335803&no_sw_cr=1#lrd=0x882bf532910f4e25:0x4aee10507689253d,3,,,,"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={styles.button}
|
|
>
|
|
Review Us on Google
|
|
</a>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|