diff --git a/next.config.mjs b/next.config.mjs index 22faeaa..ba5aaa4 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,7 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'export', - trailingSlash: true, + trailingSlash: true, images: { unoptimized: true, // ✅ needed }, diff --git a/src/app/api/reviews/route.js b/src/app/api/reviews/route.js index e8ee422..a5c77c1 100644 --- a/src/app/api/reviews/route.js +++ b/src/app/api/reviews/route.js @@ -1,32 +1,58 @@ +import { fallbackGoogleReviews } from '@/lib/reviewUtils'; + +export const dynamic = 'force-dynamic'; + +const SERPAPI_URL = 'https://serpapi.com/search.json?engine=google_maps_reviews&hl=en'; + +async function fetchGoogleReviews(apiKey, placeId) { + if (!apiKey || !placeId) { + throw new Error('Missing SERPAPI_KEY or SERPAPI_PLACE_ID'); + } + + const url = `${SERPAPI_URL}&api_key=${encodeURIComponent(apiKey)}&place_id=${encodeURIComponent(placeId)}`; + const response = await fetch(url); + + if (!response.ok) { + const body = await response.text().catch(() => ''); + throw new Error(`SerpAPI request failed ${response.status} ${response.statusText}: ${body}`); + } + + const data = await response.json(); + + if (data.error) { + throw new Error(data.error.message || data.error); + } + + if (!Array.isArray(data.reviews)) { + throw new Error('SerpAPI returned no reviews array'); + } + + return data.reviews; +} + export async function GET() { - const apiKey = process.env.SERPAPI_KEY || "6f49f63fbe238dc99a431723418b46861c1ad9fd2662cefd9f747b2d16196d3a"; - const placeId = process.env.SERPAPI_PLACE_ID || "ChIJ9b7ftlX3K4gRb7-4SkJNGCE"; + const apiKey = process.env.SERPAPI_KEY; + const placeId = process.env.SERPAPI_PLACE_ID; try { - const url = `https://serpapi.com/search.json?engine=google_maps_reviews&hl=en&api_key=${apiKey}&place_id=${placeId}`; - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`SerpAPI request failed with status ${response.status}`); - } - - const data = await response.json(); - console.log('SerpAPI reviews response', data); - - if (data.error) { - throw new Error(data.error.message || data.error); - } - - const reviews = Array.isArray(data.reviews) ? data.reviews : []; + const reviews = await fetchGoogleReviews(apiKey, placeId); return new Response(JSON.stringify({ reviews, total: reviews.length }), { status: 200, - headers: { "Content-Type": "application/json" }, + headers: { 'Content-Type': 'application/json' }, }); } catch (error) { - console.error('Error fetching reviews:', error); - return new Response(JSON.stringify({ reviews: [], error: error.message || "Failed to fetch reviews" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); + console.error('Reviews API error:', error); + return new Response( + JSON.stringify({ + reviews: fallbackGoogleReviews, + total: fallbackGoogleReviews.length, + fallback: true, + error: error.message, + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ); } } diff --git a/src/components/ReviewsSection.js b/src/components/ReviewsSection.js index 07913cc..551a110 100644 --- a/src/components/ReviewsSection.js +++ b/src/components/ReviewsSection.js @@ -2,41 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import Image from 'next/image'; import styles from './ReviewsSection.module.css'; - -function getInitials(name) { - return name - .split(' ') - .map((word) => word.charAt(0).toUpperCase()) - .slice(0, 2) - .join(''); -} - -function parseRating(review) { - const rating = review.rating ?? review.review_rating ?? review.stars ?? 0; - return Number(rating) || 0; -} - -function formatReviews(rawReviews) { - return rawReviews - .filter((review) => { - const text = review.text || review.description || review.snippet || review.review_text || review.body || review.content || ''; - return text && text.trim().length > 0 && parseRating(review) === 5; - }) - .map((review) => { - const text = review.text || review.description || review.snippet || review.review_text || review.body || review.content || ''; - const name = review.author_name || review.user?.name || 'Happy Customer'; - const rating = parseRating(review); - const role = review.relative_time_description || review.time || 'Google Reviewer'; - - return { - text, - name, - role, - initials: getInitials(name), - rating, - }; - }); -} +import { formatReviewsForHome } from '@/lib/reviewUtils'; export default function ReviewsSection() { const [current, setCurrent] = useState(0); @@ -48,11 +14,19 @@ export default function ReviewsSection() { async function loadReviews() { try { const res = await fetch('/api/reviews'); - if (!res.ok) throw new Error('Failed to fetch reviews'); - const data = await res.json(); - console.log('API Reviews Response:', data); - const formatted = formatReviews(data.reviews || []); - console.log('Formatted Reviews:', formatted); + const data = await res.json().catch(() => ({})); + + if (!res.ok) { + const errorMessage = data?.error || `${res.status} ${res.statusText}`; + console.warn('Reviews API returned non-ok response:', errorMessage); + } + + if (data.error) { + console.warn('Reviews API returned an error payload:', data.error); + } + + const reviews = Array.isArray(data.reviews) ? data.reviews : []; + const formatted = formatReviewsForHome(reviews); setReviews(formatted); } catch (error) { console.error('Reviews fetch failed:', error); diff --git a/src/components/about/AboutTestimonial.js b/src/components/about/AboutTestimonial.js index f91c4fb..7134df1 100644 --- a/src/components/about/AboutTestimonial.js +++ b/src/components/about/AboutTestimonial.js @@ -1,24 +1,7 @@ 'use client'; import { useState, useEffect } from 'react'; import styles from './AboutTestimonial.module.css'; - -function getReviewText(review) { - return review.text || review.description || review.snippet || review.review_text || review.body || review.content || ''; -} - -function formatReviews(rawReviews) { - return rawReviews - .filter((review) => { - const text = getReviewText(review); - return text && text.trim().length > 0 && review.rating >= 4; - }) - .map((review) => ({ - quote: getReviewText(review), - name: review.author_name || review.user?.name || 'Customer', - role: review.relative_time_description || 'Google Review', - rating: Math.round(review.rating || 5), - })); -} +import { formatReviewsForAbout } from '@/lib/reviewUtils'; export default function AboutTestimonial() { const [active, setActive] = useState(0); @@ -29,9 +12,18 @@ export default function AboutTestimonial() { async function loadReviews() { try { const res = await fetch('/api/reviews'); - if (!res.ok) throw new Error('Failed to fetch reviews'); - const data = await res.json(); - const formatted = formatReviews(data.reviews || []); + const data = await res.json().catch(() => ({})); + + if (!res.ok) { + const errorMessage = data?.error || `${res.status} ${res.statusText}`; + console.warn('About reviews API returned non-ok response:', errorMessage); + } + + if (data.error) { + console.warn('About reviews API returned an error payload:', data.error); + } + + const formatted = formatReviewsForAbout(Array.isArray(data.reviews) ? data.reviews : []); setReviews(formatted); } catch (error) { console.error('Failed to load reviews:', error); diff --git a/src/lib/reviewUtils.js b/src/lib/reviewUtils.js new file mode 100644 index 0000000..694362e --- /dev/null +++ b/src/lib/reviewUtils.js @@ -0,0 +1,81 @@ +export const fallbackGoogleReviews = [ + { + author_name: 'Anjali K.', + text: 'Delicious dosa with authentic masala, the best in Waterloo! The staff is friendly and the flavors are spot on.', + relative_time_description: '2 weeks ago', + rating: 5, + }, + { + author_name: 'Priya S.', + text: 'Cozy atmosphere and amazing sambar. The chutneys were fresh and the dosa was perfectly crispy.', + relative_time_description: '1 month ago', + rating: 5, + }, + { + author_name: 'Ravi M.', + text: 'I love the authentic South Indian taste here. The service was fast and the food tasted homemade.', + relative_time_description: '3 days ago', + rating: 5, + }, +]; + +export function getReviewText(review) { + return ( + review.text || + review.description || + review.snippet || + review.review_text || + review.body || + review.content || + '' + ); +} + +export function getInitials(name) { + return name + .split(' ') + .map((word) => word.charAt(0).toUpperCase()) + .slice(0, 2) + .join(''); +} + +export function parseRating(review) { + const rating = review.rating ?? review.review_rating ?? review.stars ?? 0; + return Number(rating) || 0; +} + +export function formatReviewsForHome(rawReviews) { + return rawReviews + .filter((review) => { + const text = getReviewText(review); + return text && text.trim().length > 0 && parseRating(review) >= 4; + }) + .map((review) => { + const text = getReviewText(review); + const name = review.author_name || review.user?.name || 'Happy Customer'; + const rating = parseRating(review); + const role = review.relative_time_description || review.time || 'Google Reviewer'; + + return { + text, + name, + role, + initials: getInitials(name), + rating, + }; + }); +} + +export function formatReviewsForAbout(rawReviews) { + return rawReviews + .filter((review) => { + const text = getReviewText(review); + return text && text.trim().length > 0 && parseRating(review) >= 4; + }) + .map((review) => ({ + quote: getReviewText(review), + name: review.author_name || review.user?.name || 'Customer', + role: review.relative_time_description || 'Google Review', + rating: Math.round(parseRating(review) || 5), + })); +}