Initial commit - WhyRating Engine (Google Reviews Scraper)

This commit is contained in:
Alejandro Gutiérrez
2026-02-02 18:19:00 +00:00
parent 0543a08242
commit 2206ddeff2
136 changed files with 51138 additions and 855 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import ReviewAnalytics from './ReviewAnalytics';
interface Review {
@@ -63,6 +64,9 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe
const [businessRating, setBusinessRating] = useState<number | null>(null);
const [businessImage, setBusinessImage] = useState<string | null>(null);
const [businessCategory, setBusinessCategory] = useState<string | null>(null);
// Session handoff - store session_id from validation for browser reuse
const [sessionId, setSessionId] = useState<string | null>(null);
const [sessionExpiresIn, setSessionExpiresIn] = useState<number | null>(null);
// Scraper version selection - v1.1.0 is default (multi-sort enabled)
const AVAILABLE_VERSIONS = [
@@ -309,6 +313,7 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe
}, [jobs, onJobsChange]);
// Check for reviews function (called manually when user clicks Validate)
// Uses session handoff - keeps browser alive for reuse during scraping
const checkReviews = async (query: string) => {
// Abort any previous validation request
if (abortControllerRef.current) {
@@ -323,6 +328,8 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe
setBusinessRating(null);
setBusinessImage(null);
setBusinessCategory(null);
setSessionId(null);
setSessionExpiresIn(null);
setError('');
// Create new abort controller with 60 second timeout (validation can be slow)
@@ -334,13 +341,15 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe
// Force English with hl=en parameter
const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}&hl=en`;
const response = await fetch('/api/check-reviews', {
// Use session validation endpoint - keeps browser alive for reuse
const response = await fetch('/api/sessions/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
geolocation: userFingerprint.geolocation,
browser_fingerprint: userFingerprint // Pass full fingerprint
browser_fingerprint: userFingerprint, // Pass full fingerprint
session_ttl: 300 // 5 minute session TTL
}),
signal: controller.signal,
});
@@ -350,13 +359,19 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe
const data = await response.json();
if (response.ok && data.success) {
setHasReviews(data.has_reviews);
const businessInfo = data.business_info || {};
setHasReviews(data.total_reviews > 0);
setAvailableReviewCount(data.total_reviews || 0);
setBusinessName(data.name);
setBusinessAddress(data.address);
setBusinessRating(data.rating);
setBusinessImage(data.image_url);
setBusinessCategory(data.category);
setBusinessName(businessInfo.name);
setBusinessAddress(businessInfo.address);
setBusinessRating(businessInfo.rating);
setBusinessCategory(businessInfo.category);
// Store session_id for browser reuse during scraping
if (data.session_id) {
setSessionId(data.session_id);
setSessionExpiresIn(data.expires_in);
console.log(`Session created: ${data.session_id} (expires in ${data.expires_in}s)`);
}
} else {
console.error('Failed to get business info:', data.error);
// Business not found
@@ -465,21 +480,31 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe
const url = `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(searchedQuery)}&hl=en`;
try {
// Build request body - include session_id if available for browser reuse
const requestBody: Record<string, unknown> = {
url,
business_name: businessName,
business_address: businessAddress,
rating_snapshot: businessRating,
total_reviews_snapshot: availableReviewCount,
geolocation: userFingerprint.geolocation,
browser_fingerprint: userFingerprint, // Pass full fingerprint
// Google Reviews scraper (this component is specific to Google Reviews)
job_type: 'google-reviews',
scraper_version: scraperVersion, // Selected scraper version
};
// If we have a session_id from validation, use it for browser reuse
// This saves 4-16 seconds by skipping navigation
if (sessionId) {
requestBody.session_id = sessionId;
console.log(`Using session handoff: ${sessionId}`);
}
const response = await fetch('/api/scrape', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url,
business_name: businessName,
business_address: businessAddress,
rating_snapshot: businessRating,
total_reviews_snapshot: availableReviewCount,
geolocation: userFingerprint.geolocation,
browser_fingerprint: userFingerprint, // Pass full fingerprint
// Google Reviews scraper (this component is specific to Google Reviews)
job_type: 'google-reviews',
scraper_version: scraperVersion, // Selected scraper version
}),
body: JSON.stringify(requestBody),
});
const data = await response.json();
@@ -972,6 +997,7 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe
</div>
{Array.from(jobs.values())
.filter(job => job && job.status)
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.map(job => (
<div
@@ -1278,15 +1304,14 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe
</>
)}
{/* Confirmation Modal */}
{showConfirmModal && (
{/* Confirmation Modal - rendered via portal to modal-root for proper centering */}
{showConfirmModal && typeof document !== 'undefined' && document.getElementById('modal-root') && createPortal(
<div
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
onClick={() => setShowConfirmModal(false)}
>
<div
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-2xl shadow-2xl border-2 border-green-500 animate-fade-in"
style={{ width: '400px', maxWidth: 'calc(100vw - 32px)' }}
className="bg-white rounded-2xl shadow-2xl border-2 border-green-500 animate-fade-in w-full max-w-[400px]"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
@@ -1370,7 +1395,8 @@ export default function ScraperTest({ onJobsChange, onSelectReviews }: ScraperTe
</button>
</div>
</div>
</div>
</div>,
document.getElementById('modal-root')!
)}
</div>
);