From cf5936730cd79b428ae7e45b4e6ae8318311757a Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Tue, 8 Jul 2025 14:10:08 -0700 Subject: [PATCH] Reworked initial state loading logic --- frontend/src/pages/JobAnalysisPage.tsx | 236 +++++++++++++------------ 1 file changed, 119 insertions(+), 117 deletions(-) diff --git a/frontend/src/pages/JobAnalysisPage.tsx b/frontend/src/pages/JobAnalysisPage.tsx index 0263916..ef40248 100644 --- a/frontend/src/pages/JobAnalysisPage.tsx +++ b/frontend/src/pages/JobAnalysisPage.tsx @@ -29,7 +29,7 @@ import { JobCreator } from 'components/JobCreator'; import { LoginRestricted } from 'components/ui/LoginRestricted'; import { ResumeGenerator } from 'components/ResumeGenerator'; import { JobsView } from 'components/ui/JobsView'; -import { Navigate, useNavigate, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; function WorkAddIcon(): JSX.Element { return ( @@ -91,111 +91,131 @@ const JobAnalysisPage: React.FC = () => { const [error, setError] = useState(null); const [jobTab, setJobTab] = useState('select'); - const [analysisState, setAnalysisState] = useState({ - ...initialState, - candidate: selectedCandidate, - job: selectedJob, - }); + const [analysisState, setAnalysisState] = useState(initialState); const [canAdvance, setCanAdvance] = useState(false); + const [isInitialized, setIsInitialized] = useState(false); const scrollRef = useRef(null); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const [activeStep, setActiveStep] = useState(user === null ? 0 : 1); const maxStep = 4; + // Initialize from URL params on first load only useEffect(() => { - if ( - jobId !== selectedJob?.id || - candidateId !== selectedCandidate?.id || - parseFloat(stepId || '0') !== activeStep - ) { - if (!selectedCandidate) { - navigate('/job-analysis/'); - return; - } - if (!selectedJob) { - navigate(`/job-analysis/${selectedCandidate.id}`, { replace: true }); - return; - } - if (selectedCandidate && selectedJob) { - const routeStep = activeStep ? `/${activeStep.toString()}` : ''; - navigate(`/job-analysis/${selectedCandidate.id}/${selectedJob.id}${routeStep}`, { - replace: true, - }); - } - } - }, [jobId, candidateId, selectedJob, selectedCandidate, stepId, activeStep]); + let isMounted = true; - useEffect(() => { - let routeCandidateId = candidateId || ''; - let routeJobId = jobId || ''; - const routeStepId = stepId ? `/${stepId}` : ''; - - if (routeCandidateId && routeCandidateId !== selectedCandidate?.id) { - apiClient - .getCandidate(routeCandidateId) - .then((candidate: Candidate | null) => { - if (candidate) { - setSelectedCandidate(candidate); - routeCandidateId = candidate.id || ''; - } else { + const initializeFromParams = async () => { + try { + // Initialize candidate + let candidate = selectedCandidate; + if (candidateId && candidateId !== selectedCandidate?.id) { + const fetchedCandidate = await apiClient.getCandidate(candidateId); + if (fetchedCandidate && isMounted) { + candidate = fetchedCandidate; + setSelectedCandidate(fetchedCandidate); + } else if (isMounted && !fetchedCandidate) { setError('Candidate not found.'); } - }) - .catch((err: Error) => { - console.error('Error fetching candidate:', err); - setError('Failed to fetch candidate information.'); - }); - } else { - routeCandidateId = selectedCandidate?.id || ''; - } + } else if (!candidate && user) { + // Fallback to user's candidate if no candidate selected + const userCandidate = await apiClient.getCandidate(user.id || ''); + if (userCandidate && isMounted) { + candidate = userCandidate; + setSelectedCandidate(userCandidate); + } + } - if (routeJobId && routeJobId !== selectedJob?.id) { - apiClient - .getJob(routeJobId) - .then((job: Job | null) => { - if (job) { - setSelectedJob(job); - routeJobId = job.id || ''; - } else { + // Initialize job + let job = selectedJob; + if (jobId && jobId !== selectedJob?.id) { + const fetchedJob = await apiClient.getJob(jobId); + if (fetchedJob && isMounted) { + job = fetchedJob; + setSelectedJob(fetchedJob); + } else if (isMounted && !fetchedJob) { setError('Job not found.'); } - }) - .catch((err: Error) => { - console.error('Error fetching job:', err); - setError('Failed to fetch job information.'); - }); - } else { - routeJobId = selectedJob?.id || ''; - } - }, [candidateId, jobId, setSelectedCandidate, setSelectedJob]); + } + // Initialize step + const urlStep = stepId ? parseInt(stepId, 10) : undefined; + if (urlStep !== undefined && !isNaN(urlStep) && urlStep !== activeStep) { + setActiveStep(urlStep); + } + + // Set analysis state + if (isMounted) { + setAnalysisState({ + ...initialState, + candidate, + job, + }); + setIsInitialized(true); + } + } catch (err) { + if (isMounted) { + console.error('Error during initialization:', err); + setError('Failed to initialize from URL parameters.'); + setIsInitialized(true); + } + } + }; + + initializeFromParams(); + + return () => { + isMounted = false; + }; + }, []); // Empty dependency array - only run once on mount + + // Update URL when state changes (after initialization) useEffect(() => { - if (selectedCandidate === null && user !== null) { - apiClient - .getCandidate(user.id || '') - .then((candidate: Candidate | null) => { - if (candidate) { - setSelectedCandidate(candidate); - } - }) - .catch((err: Error) => { - console.error('Error fetching candidate:', err); - setError('Failed to fetch candidate information.'); - }); + if (!isInitialized) return; + + const candidateParam = selectedCandidate?.id || ''; + const jobParam = selectedJob?.id || ''; + const stepParam = activeStep > 0 ? `/${activeStep}` : ''; + + let newPath = '/job-analysis'; + if (candidateParam) { + newPath += `/${candidateParam}`; + if (jobParam) { + newPath += `/${jobParam}${stepParam}`; + } } - }, [user, apiClient, selectedCandidate, setSelectedCandidate]); + + // Only navigate if the current path doesn't match + const currentPath = window.location.pathname; + if (currentPath !== newPath) { + navigate(newPath, { replace: true }); + } + }, [selectedCandidate, selectedJob, activeStep, isInitialized, navigate]); + + // Update analysis state when selected candidate/job changes + useEffect(() => { + if (!isInitialized) return; + + setAnalysisState(prev => ({ + ...initialState, + candidate: selectedCandidate, + job: selectedJob, + // Preserve analysis if same candidate and job + analysis: + prev.candidate === selectedCandidate && prev.job === selectedJob ? prev.analysis : null, + resume: prev.candidate === selectedCandidate && prev.job === selectedJob ? prev.resume : null, + })); + }, [selectedCandidate, selectedJob, isInitialized]); const getMissingStepRequirement = useCallback( (step: number) => { switch (step) { - case 0 /* candidate selection */: + case 0: // candidate selection break; - case 1 /* job selection */: + case 1: // job selection if (!analysisState.candidate) { return 'candidate'; } break; - case 2 /* job analysis */: + case 2: // job analysis if (!analysisState.candidate) { return 'candidate'; } @@ -203,7 +223,7 @@ const JobAnalysisPage: React.FC = () => { return 'job'; } break; - case 3 /* resume generation */: + case 3: // resume generation if (!analysisState.candidate) { return 'candidate'; } @@ -220,37 +240,21 @@ const JobAnalysisPage: React.FC = () => { [analysisState] ); - useEffect(() => { - /* Prevent recusrive state war */ - if (analysisState.candidate === selectedCandidate && analysisState.job === selectedJob) { - return; - } - const analysis = { - ...initialState, - candidate: selectedCandidate, - job: selectedJob, - }; - setAnalysisState(analysis); - }, [analysisState, selectedCandidate, selectedJob, setActiveStep, getMissingStepRequirement]); - useEffect(() => { if (activeStep === maxStep) { setCanAdvance(false); return; } const blocked = getMissingStepRequirement(activeStep + 1); - if (blocked) { - setCanAdvance(false); - } else { - setCanAdvance(true); - } + setCanAdvance(!blocked); + if (scrollRef.current) { scrollRef.current.scrollTo({ top: 0, behavior: 'smooth', }); } - }, [setCanAdvance, analysisState, activeStep, getMissingStepRequirement]); + }, [analysisState, activeStep, getMissingStepRequirement]); const handleNext = (): void => { if (activeStep === maxStep) { @@ -267,7 +271,6 @@ const JobAnalysisPage: React.FC = () => { if (activeStep === 0) { return; } - setActiveStep(prevActiveStep => prevActiveStep - 1); }; @@ -281,14 +284,10 @@ const JobAnalysisPage: React.FC = () => { }; const onCandidateSelect = (candidate: Candidate): void => { - analysisState.candidate = candidate; - setAnalysisState({ ...analysisState }); setSelectedCandidate(candidate); }; const onJobsSelected = (job: Job): void => { - analysisState.job = job; - setAnalysisState({ ...analysisState }); setSelectedJob(job); }; @@ -307,7 +306,7 @@ const JobAnalysisPage: React.FC = () => { = () => { sx={{ display: 'flex', position: 'relative', - minHeight: 0 /* Prevent flex overflow */, + minHeight: 0, maxHeight: 'min-content', flexGrow: 1, }} @@ -369,9 +368,9 @@ const JobAnalysisPage: React.FC = () => { return; } console.log('Analysis complete:', analysis); - setAnalysisState({ ...analysisState, analysis }); + setAnalysisState(prev => ({ ...prev, analysis })); }, - [analysisState] + [analysisState.analysis?.score] ); // Render function for the analysis step @@ -379,10 +378,9 @@ const JobAnalysisPage: React.FC = () => { if (!analysisState.job || !analysisState.candidate) { return ( - {JSON.stringify({ - job: analysisState.job, - candidate: analysisState.candidate, - })} + Missing required data: + {!analysisState.candidate && ' candidate'} + {!analysisState.job && ' job'} ); } @@ -410,12 +408,17 @@ const JobAnalysisPage: React.FC = () => { ); }; + // Don't render until initialized to avoid flash of incorrect state + if (!isInitialized) { + return Loading...; + } + return ( = () => { ref={scrollRef} sx={{ position: 'relative', - minHeight: 0 /* Prevent flex overflow */, - // maxHeight: 'min-content', + minHeight: 0, width: '100%', display: 'flex', flexGrow: 1,