Files
whyrating-engine-legacy/web/app/analytics/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

106 lines
4.6 KiB
TypeScript

'use client';
import Link from 'next/link';
import { useJobs } from '@/contexts/JobsContext';
import { JobStatus } from '@/components/ScraperTest';
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 AnalyticsPage() {
const { jobs, isLoading } = useJobs();
// Filter to only completed jobs with reviews
const completedJobs = jobs
.filter(j => j.status === 'completed' && j.reviews_count && j.reviews_count > 0)
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
if (isLoading) {
return (
<div className="h-full flex items-center justify-center">
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
</div>
);
}
return (
<div className="h-full overflow-y-auto p-6">
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">Analytics</h1>
<p className="text-sm text-gray-600 mt-1">
{completedJobs.length} completed {completedJobs.length === 1 ? 'scrape' : 'scrapes'} with reviews
</p>
</div>
{completedJobs.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-gray-500">
<svg className="w-20 h-20 mb-4 opacity-30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<h3 className="text-xl font-semibold text-gray-700 mb-2">No Analytics Yet</h3>
<p className="text-sm text-gray-500 mb-4">Complete a scrape job to see analytics</p>
<Link
href="/new"
className="px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Start New Scrape
</Link>
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{completedJobs.map(job => {
const businessName = extractBusinessName(job);
return (
<Link
key={job.job_id}
href={`/analytics/${job.job_id}`}
className="bg-white rounded-xl border-2 border-gray-200 p-5 hover:border-blue-400 hover:shadow-lg transition-all block"
>
<div className="flex items-start justify-between mb-3">
<h3 className="font-bold text-gray-900 truncate flex-1" title={businessName}>
{businessName}
</h3>
{job.rating_snapshot && (
<span className="flex items-center gap-1 text-yellow-600 font-semibold ml-2">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
{job.rating_snapshot.toFixed(1)}
</span>
)}
</div>
<div className="flex items-center gap-4 text-sm text-gray-600 mb-3">
<span className="font-semibold text-blue-700">{job.reviews_count} reviews</span>
{job.scrape_time && <span>{job.scrape_time.toFixed(1)}s</span>}
</div>
{job.business_category && (
<div className="mb-3">
<span className="px-2 py-0.5 bg-gray-100 text-gray-600 text-xs font-medium rounded-full">
{job.business_category}
</span>
</div>
)}
<div className="text-xs text-gray-500">
{new Date(job.created_at).toLocaleDateString()} at {new Date(job.created_at).toLocaleTimeString()}
</div>
</Link>
);
})}
</div>
)}
</div>
);
}