Files
whyrating-engine-legacy/web/contexts/JobsContext.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

107 lines
2.8 KiB
TypeScript

'use client';
import { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
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;
}
interface JobsContextType {
jobs: JobStatus[];
isLoading: boolean;
refreshJobs: () => Promise<void>;
addJob: (job: JobStatus) => void;
updateJob: (jobId: string, updates: Partial<JobStatus>) => void;
getJobById: (jobId: string) => JobStatus | undefined;
loadJobReviews: (jobId: string, previousJobId?: string) => Promise<Review[]>;
}
const JobsContext = createContext<JobsContextType | undefined>(undefined);
export function JobsProvider({ children }: { children: ReactNode }) {
const [jobs, setJobs] = useState<JobStatus[]>([]);
const [isLoading, setIsLoading] = useState(true);
const refreshJobs = useCallback(async () => {
try {
const response = await fetch('/api/jobs?limit=100');
if (response.ok) {
const data = await response.json();
if (data.jobs) {
setJobs(data.jobs);
}
}
} catch (err) {
console.error('Failed to load jobs:', err);
} finally {
setIsLoading(false);
}
}, []);
// Load jobs on mount
useEffect(() => {
refreshJobs();
}, [refreshJobs]);
const addJob = useCallback((job: JobStatus) => {
setJobs(prev => {
const existing = prev.find(j => j.job_id === job.job_id);
if (existing) {
return prev.map(j => j.job_id === job.job_id ? { ...j, ...job } : j);
}
return [job, ...prev];
});
}, []);
const updateJob = useCallback((jobId: string, updates: Partial<JobStatus>) => {
setJobs(prev => prev.map(j =>
j.job_id === jobId ? { ...j, ...updates } : j
));
}, []);
const getJobById = useCallback((jobId: string) => {
return jobs.find(j => j.job_id === jobId);
}, [jobs]);
const loadJobReviews = useCallback(async (jobId: string, previousJobId?: string): Promise<Review[]> => {
const url = previousJobId
? `/api/jobs/${jobId}/compare?previous=${previousJobId}`
: `/api/jobs/${jobId}/reviews?limit=10000`;
const response = await fetch(url);
if (!response.ok) throw new Error('Failed to fetch reviews');
const data = await response.json();
return data.reviews || [];
}, []);
return (
<JobsContext.Provider value={{
jobs,
isLoading,
refreshJobs,
addJob,
updateJob,
getJobById,
loadJobReviews,
}}>
{children}
</JobsContext.Provider>
);
}
export function useJobs() {
const context = useContext(JobsContext);
if (!context) {
throw new Error('useJobs must be used within a JobsProvider');
}
return context;
}