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>
107 lines
2.8 KiB
TypeScript
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;
|
|
}
|