Files
whyrating-engine-legacy/web/app/analytics/[id]/page.tsx
Alejandro Gutiérrez b1296059a9 Add URL-based routing with sidebar navigation
Replace client-side state switching with proper Next.js routes:
- /new - New scrape form
- /jobs - Jobs list with table view
- /jobs/[id] - Individual job details and logs
- /analytics - Analytics overview (completed jobs)
- /analytics/[id] - Analytics for specific job

Add JobsContext for shared state across routes. Update Sidebar
to use next/link with pathname matching. Root page redirects to /new.

Also adds partial job status styling to JobsView.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 10:58:48 +00:00

184 lines
6.5 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { useParams, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { useJobs } from '@/contexts/JobsContext';
import ReviewAnalytics from '@/components/ReviewAnalytics';
import { JobStatus } from '@/components/ScraperTest';
interface Review {
author: string;
rating: number;
text: string | null;
date_text: string;
avatar_url: string | null;
profile_url: string | null;
review_id: string;
is_new?: boolean;
}
function extractBusinessName(job: JobStatus): string {
if (job.business_name) return job.business_name;
try {
const urlObj = new URL(job.url);
const query = urlObj.searchParams.get('query');
return query ? decodeURIComponent(query) : 'Unknown Business';
} catch {
return 'Unknown Business';
}
}
export default function AnalyticsDetailPage() {
const params = useParams();
const searchParams = useSearchParams();
const { jobs } = useJobs();
const jobId = params.id as string;
const compareJobId = searchParams.get('compare');
const [job, setJob] = useState<JobStatus | null>(null);
const [reviews, setReviews] = useState<Review[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [newCount, setNewCount] = useState<number | undefined>(undefined);
// Find job from context or fetch it
useEffect(() => {
const foundJob = jobs.find(j => j.job_id === jobId);
if (foundJob) {
setJob(foundJob);
} else {
fetch(`/api/jobs/${jobId}`)
.then(res => res.json())
.then(data => setJob(data))
.catch(err => setError(err.message));
}
}, [jobId, jobs]);
// Fetch reviews
useEffect(() => {
if (!jobId) return;
setIsLoading(true);
setError(null);
const url = compareJobId
? `/api/jobs/${jobId}/compare?previous=${compareJobId}`
: `/api/jobs/${jobId}/reviews?limit=10000`;
fetch(url)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch reviews');
return res.json();
})
.then(data => {
setReviews(data.reviews || []);
setNewCount(data.new_count);
})
.catch(err => setError(err.message))
.finally(() => setIsLoading(false));
}, [jobId, compareJobId]);
if (isLoading) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-600">Loading analytics...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<svg className="w-16 h-16 text-red-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<h3 className="text-xl font-semibold text-gray-700 mb-2">Error Loading Analytics</h3>
<p className="text-sm text-gray-500 mb-4">{error}</p>
<Link
href="/analytics"
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Back to Analytics
</Link>
</div>
</div>
);
}
if (reviews.length === 0) {
return (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<svg className="w-16 h-16 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h3 className="text-xl font-semibold text-gray-700 mb-2">No Reviews Found</h3>
<p className="text-sm text-gray-500 mb-4">This job has no reviews to analyze</p>
<Link
href="/analytics"
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Back to Analytics
</Link>
</div>
</div>
);
}
const businessName = job ? extractBusinessName(job) : 'Business';
const businessUrl = job?.url || '';
return (
<div className="h-full overflow-y-auto p-6">
{/* Breadcrumb */}
<div className="mb-4 flex items-center gap-2 text-sm text-gray-500">
<Link href="/analytics" className="hover:text-blue-600">Analytics</Link>
<span>/</span>
<span className="text-gray-900 font-medium truncate max-w-[200px]" title={businessName}>
{businessName}
</span>
</div>
{/* Header with back button */}
<div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-bold text-gray-900">Analytics</h1>
<div className="flex gap-2">
<Link
href={`/jobs/${jobId}`}
className="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg font-medium transition-colors flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
View Job Details
</Link>
<Link
href="/analytics"
className="px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg font-medium transition-colors flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
Back to Analytics
</Link>
</div>
</div>
<ReviewAnalytics
reviews={reviews}
businessName={businessName}
businessUrl={businessUrl}
newCount={newCount}
businessCategory={job?.business_category || undefined}
reviewTopics={job?.review_topics || undefined}
/>
</div>
);
}