diff --git a/package-lock.json b/package-lock.json index 548d0c1..f3495ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "react-google-recaptcha": "^3.1.0", "react-icons": "^5.5.0", "sitemap": "^9.0.0", + "swiper": "^12.0.3", "xml2js": "^0.6.2" }, "devDependencies": { @@ -9369,6 +9370,25 @@ "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": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", diff --git a/package.json b/package.json index 06154f7..5aea008 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react-google-recaptcha": "^3.1.0", "react-icons": "^5.5.0", "sitemap": "^9.0.0", + "swiper": "^12.0.3", "xml2js": "^0.6.2" }, "devDependencies": { @@ -36,4 +37,4 @@ "selenium-webdriver": "^4.38.0", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/src/app/about-antalya-restaurant/AboutContent.tsx b/src/app/about-antalya-restaurant/AboutContent.tsx index eb4dcd1..3ae6356 100644 --- a/src/app/about-antalya-restaurant/AboutContent.tsx +++ b/src/app/about-antalya-restaurant/AboutContent.tsx @@ -5,12 +5,61 @@ import FAQ from "@/components/FAQ/FAQ"; import Image from "next/image"; import Link from "next/link"; 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 { 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() { - const [currentTestimonial, setCurrentTestimonial] = useState(0); + const [reviews, setReviews] = useState([]); + const [loading, setLoading] = useState(true); + const [expandedReview, setExpandedReview] = useState(null); + const [swiperInstance, setSwiperInstance] = useState(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 const fadeInUp = { @@ -65,22 +114,58 @@ export default function AboutContent() { } }; - // Auto-slide testimonials useEffect(() => { - const interval = setInterval(() => { - setCurrentTestimonial((prev) => (prev + 1) % testimonialData.length); - }, 5000); // Change every 5 seconds - - return () => clearInterval(interval); + async function loadReviews() { + try { + const res = await fetch("/api/reviews"); + if (!res.ok) throw new Error("Failed to fetch"); + 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 = () => { - setCurrentTestimonial((prev) => (prev + 1) % testimonialData.length); - }; + const displayedReviews = reviews.length > 0 && reviews.length < 3 + ? [...reviews, ...reviews, ...reviews] + : reviews; + + function renderStars(rating: number) { + return [...Array(5)].map((_, i) => ( + + )); + } + + 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 (
@@ -212,67 +297,92 @@ export default function AboutContent() { ANTALYA Testimonials Section Cutlery Icon - - What Our Guests Say - - - +

What Our Guests Say

- - -

"{testimonialData[currentTestimonial].text}"

-
- {'★★★★★'} +
+ + +
+ {loading ? ( +
+

Loading reviews...

- {/*
-
- {testimonialData[currentTestimonial].name} -
-
-

{testimonialData[currentTestimonial].name}

-

{testimonialData[currentTestimonial].role}

-
-
*/} - - + ) : ( + + {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 ( + +
+
+
+ {profileImg ? ( + {name} ((e.target as HTMLImageElement).src = '/images/placeholder.png')} + /> + ) : ( +
+ {getInitials(name)} +
+ )} +
+
+

{name}

+
+ {renderStars(r.rating)} +
+
+
- +

+ "{isExpanded ? fullText : truncateText(fullText)}" +

- {/* Slider dots */} -
- {testimonialData.map((_, index) => ( - + )} +
+ + ); + })} + + )} +
+ +
diff --git a/src/app/about-antalya-restaurant/about.module.css b/src/app/about-antalya-restaurant/about.module.css index 230a890..359b19d 100644 --- a/src/app/about-antalya-restaurant/about.module.css +++ b/src/app/about-antalya-restaurant/about.module.css @@ -56,11 +56,11 @@ } .testimonialsSection { + padding: 80px 20px; background-color: #3a0c08; background-image: url('/images/about-us/guest-bg.webp'); background-size: cover; background-position: center; - background-repeat: repeat; } .container { @@ -149,6 +149,13 @@ margin-bottom: 1rem; } +.stars { + display: flex; + gap: 5px; + color: var(--color-paragraph); + font-size: 1.2rem; +} + /* Features Section */ .featuresGrid { display: grid; @@ -214,9 +221,9 @@ background: transparent; border: 2px solid #b07c4b; color: var(--color-alterparagraph) !important; - font-size: 2.5rem; - width: 50px; - height: 50px; + font-size: 1.2rem; + width: 40px; + height: 40px; display: flex; align-items: center; justify-content: center; @@ -230,11 +237,11 @@ } .sliderBtn:first-child { - left: 10px; + left: 20px; } .sliderBtn:last-child { - right: 10px; + right: 20px; } .sliderBtn:hover { @@ -245,21 +252,24 @@ .testimonialCard { background-color: #F5E6D3; - padding: 3rem 2.5rem; + padding: 2rem 2.5rem; border: 2px solid #b07c4b; display: flex; flex-direction: column; - gap: 2rem; + gap: 1.5rem; flex: 1; - min-height: 300px; + min-height: 400px; + justify-content: center; + align-items: center; } .testimonialText { font-family: var(--font-lato); - font-size: 1.3rem; + font-size: 1.15rem; line-height: 1.8; font-style: italic; text-align: center; + color: #a67c52; } .testimonialAuthor { @@ -292,9 +302,10 @@ .authorName { font-family: var(--font-playfair); - font-size: 1.2rem; + font-size: 1.1rem; color: #3e2723; font-weight: 600; + margin-bottom: 10px; } .authorRole { @@ -333,7 +344,7 @@ .button { display: inline-block; - margin-top: 3rem; + /* margin-top: 3rem; */ padding: 0.8rem 2rem; border: 1px solid var(--color-gold); color: #d3cab3; diff --git a/src/app/api/reviews/route.ts b/src/app/api/reviews/route.ts new file mode 100644 index 0000000..7e48327 --- /dev/null +++ b/src/app/api/reviews/route.ts @@ -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 }); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index 7fffa72..b5c7c0f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -290,4 +290,117 @@ h2 { 100% { 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; } \ No newline at end of file diff --git a/src/components/Blogs/Blogs.module.css b/src/components/Blogs/Blogs.module.css index 3c748f5..3226dc2 100644 --- a/src/components/Blogs/Blogs.module.css +++ b/src/components/Blogs/Blogs.module.css @@ -226,7 +226,7 @@ } } -@media (max-width: 480px) { +@media (max-width: 500px) { .sliderContainer { padding: 0 15px; } diff --git a/src/components/Catering/Catering.module.css b/src/components/Catering/Catering.module.css index 366814b..a85ead6 100644 --- a/src/components/Catering/Catering.module.css +++ b/src/components/Catering/Catering.module.css @@ -407,8 +407,8 @@ margin-left: 10%; right: auto; bottom: auto; - border-top: 10px solid #000000; - border-left: 10px solid #000000; + border-top: 10px solid #c49c5c; + border-left: 10px solid #c49c5c; } .callWidgetLeft { diff --git a/src/components/Testimonials/Testimonials.module.css b/src/components/Testimonials/Testimonials.module.css index ba6cf60..a13775b 100644 --- a/src/components/Testimonials/Testimonials.module.css +++ b/src/components/Testimonials/Testimonials.module.css @@ -39,6 +39,7 @@ .sliderContainer { display: flex; justify-content: center; + align-items: center; gap: 30px; max-width: 1400px; width: 100%; @@ -57,22 +58,16 @@ background: #242323; border: 14px solid rgb(196, 156, 92); border-radius: 30px; - padding: 40px 30px; + padding: 30px 20px; text-align: center; position: relative; - /* Flex basis for 3 items: 100% / 3 = 33.333% */ - flex: 0 0 33.333%; - max-width: 33.333%; display: flex; flex-direction: column; align-items: center; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); box-sizing: border-box; - margin: 0 15px; - /* Visual gap */ - /* Adjust flex basis to account for margin: calc(33.333% - 30px) */ - flex: 0 0 calc(33.333% - 30px); - max-width: calc(33.333% - 30px); + height: 100%; + width: 100%; } .avatarContainer { @@ -83,7 +78,9 @@ border-radius: 50%; padding: 7px; background: rgb(196, 156, 92); - /* Gold gradient border effect */ + display: flex; + align-items: center; + justify-content: center; } .avatar { @@ -103,58 +100,93 @@ overflow: hidden; } +.avatar, .authorImage { + width: 100%; + height: 100%; border-radius: 50%; object-fit: cover; + background-color: #333; } .name { font-family: var(--font-inter), sans-serif; - /* Assuming Inter or similar */ font-size: 1.1rem; color: #f5e6d3; - margin-bottom: 30px; - font-weight: 500; + /* White as seen in image */ + margin-bottom: 5px; + font-weight: 600; } .text { - font-family: var(--font-playfair); - font-size: 1.2rem; - line-height: 1.6; - color: #e0e0e0; - margin-bottom: 30px; + font-family: var(--font-paragraph); + color: var(--color-paragraph); + font-size: 1.15rem; + line-height: 1.7; + /* White as seen in image */ + margin-bottom: 20px; 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 { color: var(--color-paragraph); - font-size: 1.5rem; - letter-spacing: 5px; + font-size: 16px; + display: flex; + justify-content: center; + gap: 3px; + margin-bottom: 15px; } -.arrow { - position: absolute; - top: 50%; - transform: translateY(-50%); - color: var(--color-paragraph); - font-size: 3rem; - cursor: pointer; - background: none; +.navBtn { + background: transparent; border: none; - transition: transform 0.2s; + color: rgb(196, 156, 92); + font-size: 2.5rem; + cursor: pointer; + transition: all 0.3s; z-index: 10; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + width: 50px; + height: 50px; } -.arrow:hover { - transform: translateY(-50%) scale(1.1); -} - -.prevArrow { - left: 0; -} - -.nextArrow { - right: 0; +.navBtn:hover { + transform: scale(1.2); + color: #fff; } /* Lanterns decoration placeholder */ @@ -191,40 +223,44 @@ .sliderContainer { padding: 0 50px; } - - .arrow { - font-size: 2.8rem; - } } @media (max-width: 1200px) { .sliderContainer { padding: 0 40px; } +} - .arrow { - font-size: 2.5rem; +@media (min-width: 1025px) { + .navBtn { + position: absolute; + top: 50%; + transform: translateY(-50%); + } + + .prevArrow { + left: 0; + } + + .nextArrow { + right: 0; } } - - @media (max-width: 1024px) { .sliderContainer { - padding: 0 30px; + padding: 0 45px; } - .card { - flex: 0 0 100%; - max-width: 50%; - margin: 0; + .title { + font-size: 2.5rem; + margin-bottom: 40px; } - .card:nth-child(3) { - display: none; - } - - .arrow { + .navBtn { + position: absolute; + top: 50%; + transform: translateY(-50%); font-size: 2.2rem; } @@ -235,20 +271,30 @@ .nextArrow { right: 5px; } - - .title { - font-size: 2.5rem; - margin-bottom: 40px; - } } @media (max-width: 768px) { .sliderContainer { - padding: 0 20px; + padding: 0 35px; } - .arrow { - font-size: 2rem; + .card { + 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 { @@ -262,28 +308,23 @@ @media (max-width: 480px) { .sliderContainer { - padding: 0 15px; - } - - .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; + padding: 0 30px; } .title { - font-size: 2rem; + font-size: 2.2rem; + } + + .section { + padding: 60px 10px; + } + + .card { + border-width: 8px; + border-radius: 20px; + } + + .navBtn { + font-size: 1.5rem; } } \ No newline at end of file diff --git a/src/components/Testimonials/Testimonials.tsx b/src/components/Testimonials/Testimonials.tsx index fefdd99..2be3afd 100644 --- a/src/components/Testimonials/Testimonials.tsx +++ b/src/components/Testimonials/Testimonials.tsx @@ -3,41 +3,154 @@ import { useState, useEffect } from 'react' import Image from 'next/image' 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() { - const [currentIndex, setCurrentIndex] = useState(0) + const [reviews, setReviews] = useState([]); + const [loading, setLoading] = useState(true); + const [expandedReview, setExpandedReview] = useState(null); + const [swiperInstance, setSwiperInstance] = useState(null); - const nextSlide = () => { - setCurrentIndex((prev) => (prev + 1) % (testimonials.length - 2)) - } - - const prevSlide = () => { - setCurrentIndex((prev) => (prev === 0 ? testimonials.length - 3 : prev - 1)) - } - - // Auto-slide effect - useEffect(() => { - const interval = setInterval(() => { - nextSlide() - }, 5000) // Change slide every 5 seconds - - return () => clearInterval(interval) - }, []) - - // Get the 3 visible testimonials - const getVisibleTestimonials = () => { - const items = [] - for (let i = 0; i < 3; i++) { - const index = (currentIndex + i) % testimonials.length - items.push(testimonials[index]) + 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, + }, } - 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) => ( + + )); } - 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 (
@@ -49,31 +162,76 @@ export default function Testimonials() {

Testimonials

- - - {visibleItems.map((item) => ( -
-
-
-
- {`${item.name} -
-
-
- {/*

— {item.name}

*/} -

"{item.text}"

-
- {'★★★★★'} -
+ {loading ? ( +
+

Loading reviews...

- ))} + ) : ( + <> + + + {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 ( + +
+
+
+ {profileImg ? ( + {name} ((e.target as HTMLImageElement).src = '/images/placeholder.png')} + /> + ) : ( +
+ {getInitials(name)} +
+ )} +
+
+ +

{name}

+ +
+ {renderStars(r.rating)} +
+ +

+ "{isExpanded ? fullText : truncateText(fullText)}" +

+ + + + +
+
+ ); + })} +
+ + + )}