live google fetch reviews updated
This commit is contained in:
parent
0d2b4bb5ba
commit
198bc1df03
@ -1,35 +1,38 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Autoplay, Navigation, Pagination } from "swiper/modules";
|
import { Autoplay } from "swiper/modules";
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/autoplay";
|
||||||
|
|
||||||
const swiperOptions = {
|
const swiperOptions = {
|
||||||
modules: [Autoplay, Pagination, Navigation],
|
modules: [Autoplay],
|
||||||
slidesPerView: 1,
|
slidesPerView: 3,
|
||||||
spaceBetween: 30,
|
spaceBetween: 20,
|
||||||
loop: true,
|
loop: true,
|
||||||
|
|
||||||
autoplay: {
|
autoplay: {
|
||||||
delay: 3000,
|
delay: 2000,
|
||||||
disableOnInteraction: false,
|
disableOnInteraction: false,
|
||||||
|
pauseOnMouseEnter: false,
|
||||||
},
|
},
|
||||||
|
breakpoints: {
|
||||||
navigation: {
|
0: { slidesPerView: 1 },
|
||||||
nextEl: '.srn',
|
768: { slidesPerView: 2 },
|
||||||
prevEl: '.srp',
|
1024: { slidesPerView: 3 },
|
||||||
},
|
|
||||||
|
|
||||||
pagination: {
|
|
||||||
el: '.swiper-pagination',
|
|
||||||
clickable: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Testimonial() {
|
export default function Testimonial() {
|
||||||
|
|
||||||
const [reviews, setReviews] = useState([]);
|
const [reviews, setReviews] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [expandedReview, setExpandedReview] = useState(null);
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function loadReviews() {
|
async function loadReviews() {
|
||||||
@ -37,29 +40,17 @@ export default function Testimonial() {
|
|||||||
const res = await fetch("/api/reviews");
|
const res = await fetch("/api/reviews");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
// FILTER EMPTY REVIEWS (IMPORTANT) & must be positive review: rating 4 or 5
|
|
||||||
const cleaned = (data.reviews || []).filter(r =>
|
const cleaned = (data.reviews || []).filter(r =>
|
||||||
|
|
||||||
(r.text ||
|
(r.text ||
|
||||||
r.description ||
|
r.description ||
|
||||||
r.snippet ||
|
r.snippet ||
|
||||||
r.review_text ||
|
r.review_text ||
|
||||||
r.body ||
|
r.body ||
|
||||||
r.content) &&
|
r.content) &&
|
||||||
(r.text?.trim() !== "" ||
|
|
||||||
r.description?.trim() !== "" ||
|
|
||||||
r.snippet?.trim() !== "" ||
|
|
||||||
r.review_text?.trim() !== "" ||
|
|
||||||
r.body?.trim() !== "" ||
|
|
||||||
r.content?.trim() !== "") &&
|
|
||||||
|
|
||||||
|
|
||||||
r.rating >= 4
|
r.rating >= 4
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
setReviews(cleaned);
|
setReviews(cleaned);
|
||||||
// console.log(cleaned);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch reviews", error);
|
console.error("Failed to fetch reviews", error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -69,176 +60,129 @@ export default function Testimonial() {
|
|||||||
loadReviews();
|
loadReviews();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Function to render stars based on rating
|
const displayedReviews = reviews.length > 0 && reviews.length < 3
|
||||||
|
? [...reviews, ...reviews, ...reviews]
|
||||||
|
: reviews;
|
||||||
|
|
||||||
function renderStars(rating) {
|
function renderStars(rating) {
|
||||||
return [...Array(5)].map((_, i) => (
|
return [...Array(5)].map((_, i) => (
|
||||||
<span
|
<span key={i} className={`fa fa-star ${i < rating ? "text-warning" : ""}`}></span>
|
||||||
key={i}
|
|
||||||
className={`fa fa-star ${i < rating ? "text-warning" : ""}`}
|
|
||||||
></span>
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getReviewText(r) {
|
||||||
|
return r.text || r.description || r.snippet || r.review_text || r.body || r.content || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncateText(text) {
|
||||||
|
return text.length > 150 ? text.substring(0, 150) + "..." : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProfileImage(r) {
|
||||||
|
// Normalize profile photo URLs
|
||||||
|
const url = r.profile_photo_url || r.author_profile_photo_url || r.user?.thumbnail;
|
||||||
|
if (!url) return "/default-user.png";
|
||||||
|
return url.startsWith("http") ? url : `https://lh3.googleusercontent.com/${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section className="testimonial-section-two pb-0">
|
||||||
|
<div
|
||||||
|
className="icon-layer-two"
|
||||||
|
style={{ backgroundImage: "url(/assets/images/home/review-right-bottom-element-home.webp)" }}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div className="auto-container">
|
||||||
|
<div className="sec-title centered">
|
||||||
|
<div className="title">Google Reviews</div>
|
||||||
|
<h2>Hear from our happy customers</h2>
|
||||||
|
<div className="separate"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Testimonial Section Two */}
|
<div className="inner-container">
|
||||||
<section className="testimonial-section-two pb-0">
|
{loading && <p className="text-center">Loading reviews...</p>}
|
||||||
<div
|
|
||||||
className="icon-layer-two"
|
|
||||||
style={{ backgroundImage: "url(/assets/images/home/review-right-bottom-element-home.webp)" }}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div className="auto-container">
|
{!loading && isClient && displayedReviews.length > 0 && (
|
||||||
{/* Section Title */}
|
|
||||||
<div className="sec-title centered">
|
|
||||||
<div className="title">Google Reviews</div>
|
|
||||||
<h2>Hear from our happy customers</h2>
|
|
||||||
<div className="separate"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="inner-container">
|
|
||||||
<Swiper {...swiperOptions} className="single-item-carousel">
|
<Swiper {...swiperOptions} className="single-item-carousel">
|
||||||
{/* Testimonial Block Two */}
|
{displayedReviews.map((r, index) => {
|
||||||
|
const fullText = getReviewText(r);
|
||||||
|
const isExpanded = expandedReview === index;
|
||||||
|
|
||||||
{loading && (
|
return (
|
||||||
<p className="text-center">Loading reviews...</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Dynamic Reviews from API */}
|
|
||||||
{!loading && reviews.length > 0 &&
|
|
||||||
reviews.map((r, index) => (
|
|
||||||
<SwiperSlide key={index}>
|
<SwiperSlide key={index}>
|
||||||
<div className="testimonial-block-two">
|
<div className="google-review-card equal-height">
|
||||||
<div className="inner-box">
|
<div className="google-review-header">
|
||||||
<div className="rating gap-1 mb-3">
|
<div className="google-avatar">
|
||||||
{renderStars(r.rating)}
|
<img
|
||||||
|
src={getProfileImage(r)}
|
||||||
|
alt={r.author_name || r.user?.name || "User"}
|
||||||
|
onError={(e) => (e.target.src = "/default-user.png")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text">
|
|
||||||
“
|
<div className="google-user-info">
|
||||||
{r.text ||
|
<h4 className="google-name">
|
||||||
r.description ||
|
{r.author_name || r.user?.name || "Customer"}
|
||||||
r.snippet ||
|
</h4>
|
||||||
r.review_text ||
|
<div className="google-stars">{renderStars(r.rating)}</div>
|
||||||
r.body ||
|
|
||||||
r.content}
|
|
||||||
”
|
|
||||||
</div>
|
</div>
|
||||||
<div className="designation">— {r.user?.name}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className="google-text">
|
||||||
|
{isExpanded ? fullText : truncateText(fullText)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{r.images &&
|
||||||
|
r.images.length > 0 &&
|
||||||
|
r.images.some(img => img && img !== "ky") && (
|
||||||
|
<div className="google-review-images">
|
||||||
|
{r.images.map((img, i) => {
|
||||||
|
if (!img || img === "ky") return null;
|
||||||
|
const fixedImg = img.startsWith("http")
|
||||||
|
? img
|
||||||
|
: `https://lh3.googleusercontent.com/${img}`;
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
key={i}
|
||||||
|
src={fixedImg}
|
||||||
|
alt="Review image"
|
||||||
|
className="google-review-photo"
|
||||||
|
onError={(e) => (e.target.style.display = "none")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fullText.length > 150 && (
|
||||||
|
<button
|
||||||
|
className="read-more-btn mt-3"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setExpandedReview(isExpanded ? null : index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isExpanded ? "Read Less" : "Read More"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))
|
);
|
||||||
}
|
})}
|
||||||
|
|
||||||
{/* <SwiperSlide>
|
|
||||||
<div className="testimonial-block-two">
|
|
||||||
<div className="inner-box">
|
|
||||||
<div className="rating gap-1 mb-3">
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
</div>
|
|
||||||
<div className="text">
|
|
||||||
“Absolutely love this place! Every blend tastes fresh and natural. The flavors pop, and you can really tell they use quality ingredients. Sixty5 Street never disappoints.”
|
|
||||||
</div>
|
|
||||||
<div className="designation">— Emily R.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
|
|
||||||
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="testimonial-block-two">
|
|
||||||
<div className="inner-box">
|
|
||||||
<div className="rating gap-1 mb-3">
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
</div>
|
|
||||||
<div className="text">
|
|
||||||
“The perfect spot when you need something refreshing. Their fruit mixes are vibrant, clean, and full of energy. I always leave feeling great.”
|
|
||||||
</div>
|
|
||||||
<div className="designation">— Jason M.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="testimonial-block-two">
|
|
||||||
<div className="inner-box">
|
|
||||||
<div className="rating gap-1 mb-3">
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
</div>
|
|
||||||
<div className="text">
|
|
||||||
“Sixty5 Street has mastered the art of fresh flavor. The bowls are colorful, the drinks are delicious, and everything feels thoughtfully prepared. A must-try!”
|
|
||||||
</div>
|
|
||||||
<div className="designation">— Sofia L.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="testimonial-block-two">
|
|
||||||
<div className="inner-box">
|
|
||||||
<div className="rating gap-1 mb-3">
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
</div>
|
|
||||||
<div className="text">
|
|
||||||
“Consistently amazing! The blends are smooth, balanced, and not overly sweet. You can taste the real fruit in every sip. Highly recommend for healthy cravings.”
|
|
||||||
</div>
|
|
||||||
<div className="designation">— David P.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
|
|
||||||
<SwiperSlide>
|
|
||||||
<div className="testimonial-block-two">
|
|
||||||
<div className="inner-box">
|
|
||||||
<div className="rating gap-1 mb-3">
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
<span className="fa fa-star"></span>
|
|
||||||
</div>
|
|
||||||
<div className="text">
|
|
||||||
“Super fresh, super tasty. The street-style vibe makes the whole experience fun and lively. Sixty5 Street has quickly become one of my favorite places to grab a flavorful drink.”
|
|
||||||
</div>
|
|
||||||
<div className="designation">— Ava T.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SwiperSlide> */}
|
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Google Review Button */}
|
<div className="btns-box text-center mt-4">
|
||||||
<div className="btns-box text-center mt-4">
|
<Link
|
||||||
<Link
|
href="https://www.google.com/search?sca_esv=0fe16c1f02c217b7&sxsrf=AE3TifOLptPQLUmmtN31E_3elXLW6TFOQw:1762618049873&si=AMgyJEtREmoPL4P1I5IDCfuA8gybfVI2d5Uj7QMwYCZHKDZ-E-aY0flGiK9jtBbvWKno0yJxYW9CK-ZYgm0G70i4ON2SMlNBNsid-fMvQPqNzI7FcY1u8NR67M0xsy1G8HMAZhtgOP2m&q=Sixty5+Street+Reviews&sa=X&ved=2ahUKEwjY5_L19-KQAxW89DgGHdw0AesQ0bkNegQILRAE&biw=1366&bih=633&dpr=1&sei=MHYRadbjFKWo4-EPlrjFkAs&zx=1762752154509&no_sw_cr=1#lrd=0x882b3dbb0e18ed73:0xbdb3783d6e6393c9,3,,,,"
|
||||||
href="https://www.google.com/search?sca_esv=0fe16c1f02c217b7&sxsrf=AE3TifOLptPQLUmmtN31E_3elXLW6TFOQw:1762618049873&si=AMgyJEtREmoPL4P1I5IDCfuA8gybfVI2d5Uj7QMwYCZHKDZ-E-aY0flGiK9jtBbvWKno0yJxYW9CK-ZYgm0G70i4ON2SMlNBNsid-fMvQPqNzI7FcY1u8NR67M0xsy1G8HMAZhtgOP2m&q=Sixty5+Street+Reviews&sa=X&ved=2ahUKEwjY5_L19-KQAxW89DgGHdw0AesQ0bkNegQILRAE&biw=1366&bih=633&dpr=1&sei=MHYRadbjFKWo4-EPlrjFkAs&zx=1762752154509&no_sw_cr=1#lrd=0x882b3dbb0e18ed73:0xbdb3783d6e6393c9,3,,,,"
|
target="_blank"
|
||||||
target="_blank"
|
className="theme-btn btn-style-one clearfix"
|
||||||
className="theme-btn btn-style-one clearfix"
|
>
|
||||||
>
|
<span className="icon"></span> Review us on Google
|
||||||
<span className="icon"></span> Review us on Google
|
</Link>
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
{/* End Testimonial Section Two */}
|
</section>
|
||||||
|
);
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user