Initial commit - WhyRating Engine (Google Reviews Scraper)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user