187 lines
6.6 KiB
TypeScript
187 lines
6.6 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(async res => {
|
|
if (!res.ok) {
|
|
const errorData = await res.json().catch(() => ({}));
|
|
throw new Error(errorData.error || `Failed to fetch reviews (${res.status})`);
|
|
}
|
|
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>
|
|
);
|
|
}
|