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>
This commit is contained in:
106
web/contexts/JobsContext.tsx
Normal file
106
web/contexts/JobsContext.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'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;
|
||||
}
|
||||
Reference in New Issue
Block a user