live google fetch reviews updated

This commit is contained in:
akash 2025-11-17 18:47:23 +05:30
parent 0d2b4bb5ba
commit 198bc1df03
2 changed files with 583 additions and 513 deletions

View File

@ -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,21 +60,32 @@ 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 (
<>
{/* Testimonial Section Two */}
<section className="testimonial-section-two pb-0"> <section className="testimonial-section-two pb-0">
<div <div
className="icon-layer-two" className="icon-layer-two"
@ -91,7 +93,6 @@ export default function Testimonial() {
></div> ></div>
<div className="auto-container"> <div className="auto-container">
{/* Section Title */}
<div className="sec-title centered"> <div className="sec-title centered">
<div className="title">Google Reviews</div> <div className="title">Google Reviews</div>
<h2>Hear from our happy customers</h2> <h2>Hear from our happy customers</h2>
@ -99,132 +100,78 @@ export default function Testimonial() {
</div> </div>
<div className="inner-container"> <div className="inner-container">
<Swiper {...swiperOptions} className="single-item-carousel"> {loading && <p className="text-center">Loading reviews...</p>}
{/* Testimonial Block Two */}
{loading && ( {!loading && isClient && displayedReviews.length > 0 && (
<p className="text-center">Loading reviews...</p> <Swiper {...swiperOptions} className="single-item-carousel">
{displayedReviews.map((r, index) => {
const fullText = getReviewText(r);
const isExpanded = expandedReview === index;
return (
<SwiperSlide key={index}>
<div className="google-review-card equal-height">
<div className="google-review-header">
<div className="google-avatar">
<img
src={getProfileImage(r)}
alt={r.author_name || r.user?.name || "User"}
onError={(e) => (e.target.src = "/default-user.png")}
/>
</div>
<div className="google-user-info">
<h4 className="google-name">
{r.author_name || r.user?.name || "Customer"}
</h4>
<div className="google-stars">{renderStars(r.rating)}</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>
)} )}
{/* Dynamic Reviews from API */} {fullText.length > 150 && (
{!loading && reviews.length > 0 && <button
reviews.map((r, index) => ( className="read-more-btn mt-3"
<SwiperSlide key={index}> onClick={(e) => {
<div className="testimonial-block-two"> e.stopPropagation();
<div className="inner-box"> setExpandedReview(isExpanded ? null : index);
<div className="rating gap-1 mb-3"> }}
{renderStars(r.rating)} >
</div> {isExpanded ? "Read Less" : "Read More"}
<div className="text"> </button>
)}
{r.text ||
r.description ||
r.snippet ||
r.review_text ||
r.body ||
r.content}
</div>
<div className="designation"> {r.user?.name}</div>
</div>
</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,,,,"
@ -237,8 +184,5 @@ export default function Testimonial() {
</div> </div>
</div> </div>
</section> </section>
{/* End Testimonial Section Two */} );
</>
)
} }

View File

@ -104,12 +104,19 @@ a{
} }
button, button,
a:hover,a:focus,a:visited{ a:hover,
a:focus,
a:visited {
text-decoration: none; text-decoration: none;
outline: none !important; outline: none !important;
} }
h1,h2,h3,h4,h5,h6 { h1,
h2,
h3,
h4,
h5,
h6 {
position: relative; position: relative;
font-weight: normal; font-weight: normal;
margin: 0px; margin: 0px;
@ -118,7 +125,10 @@ h1,h2,h3,h4,h5,h6 {
font-family: var(--poppins); font-family: var(--poppins);
} }
input,button,select,textarea{ input,
button,
select,
textarea {
font-family: var(--poppins); font-family: var(--poppins);
} }
@ -189,7 +199,8 @@ h6{
min-width: 300px; min-width: 300px;
} }
ul,li{ ul,
li {
list-style: none; list-style: none;
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
@ -537,7 +548,19 @@ img{
color: #cf2d1f; color: #cf2d1f;
} }
.preloader{ position:fixed; left:0px; top:0px; width:100%; height:100%; z-index:999999; background-color:#ffffff; background-position:center center; background-repeat:no-repeat; background-image:url(../images/icons/preloader.svg); background-size:100px; } .preloader {
position: fixed;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
z-index: 999999;
background-color: #ffffff;
background-position: center center;
background-repeat: no-repeat;
background-image: url(../images/icons/preloader.svg);
background-size: 100px;
}
img { img {
display: inline-block; display: inline-block;
@ -681,3 +704,106 @@ img{
.sec-title-two.centered { .sec-title-two.centered {
text-align: center !important; text-align: center !important;
} }
.google-review-card {
background: #fff;
border-radius: 14px;
padding: 20px;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.08);
border: 1px solid #eee;
height: 100%;
}
.google-review-header {
display: flex;
align-items: flex-start;
gap: 15px;
margin-bottom: 10px;
}
.google-avatar {
position: relative;
width: 52px;
height: 52px;
border-radius: 50%;
background: #e0e0e0;
color: #333;
font-weight: 600;
font-size: 22px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.google-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.google-g-icon {
width: 18px;
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
}
.google-name {
font-size: 16px;
font-weight: 600;
margin: 0;
margin-bottom: 2px;
}
.google-stars span {
font-size: 15px;
margin-right: 2px;
}
.google-text {
font-size: 14px;
color: #444;
margin-top: 8px;
line-height: 1.5;
}
.google-readmore {
font-size: 14px;
color: #1a73e8;
cursor: pointer;
margin-top: 6px;
display: inline-block;
}
.google-review-images {
display: flex;
gap: 10px;
margin-top: 10px;
flex-wrap: wrap;
}
.google-review-photo {
width: 80px;
height: 80px;
border-radius: 8px;
object-fit: cover;
border: 1px solid #eee;
}
.equal-height {
min-height: 350px;
display: flex;
flex-direction: column;
}
.read-more-btn {
background: none;
border: none;
color: #cf2d1f;
font-weight: 600;
cursor: pointer;
margin-top: 5px;
padding: 0;
}