Reworked initial state loading logic

This commit is contained in:
James Ketr 2025-07-08 14:10:08 -07:00
parent 89bcc1cb55
commit cf5936730c

View File

@ -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<BackstoryPageProps> = () => {
const [error, setError] = useState<string | null>(null);
const [jobTab, setJobTab] = useState<string>('select');
const [analysisState, setAnalysisState] = useState<AnalysisState>({
...initialState,
candidate: selectedCandidate,
job: selectedJob,
});
const [analysisState, setAnalysisState] = useState<AnalysisState>(initialState);
const [canAdvance, setCanAdvance] = useState<boolean>(false);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const scrollRef = useRef<HTMLDivElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [activeStep, setActiveStep] = useState<number>(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<BackstoryPageProps> = () => {
return 'job';
}
break;
case 3 /* resume generation */:
case 3: // resume generation
if (!analysisState.candidate) {
return 'candidate';
}
@ -220,37 +240,21 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
[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<BackstoryPageProps> = () => {
if (activeStep === 0) {
return;
}
setActiveStep(prevActiveStep => prevActiveStep - 1);
};
@ -281,14 +284,10 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
};
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<BackstoryPageProps> = () => {
<Box
sx={{
width: '100%',
minHeight: 0 /* Prevent flex overflow */,
minHeight: 0,
maxHeight: 'min-content',
position: 'relative',
display: 'flex',
@ -334,7 +333,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
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<BackstoryPageProps> = () => {
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<BackstoryPageProps> = () => {
if (!analysisState.job || !analysisState.candidate) {
return (
<Box>
{JSON.stringify({
job: analysisState.job,
candidate: analysisState.candidate,
})}
Missing required data:
{!analysisState.candidate && ' candidate'}
{!analysisState.job && ' job'}
</Box>
);
}
@ -410,12 +408,17 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
);
};
// Don't render until initialized to avoid flash of incorrect state
if (!isInitialized) {
return <Box>Loading...</Box>;
}
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
height: '100%' /* Restrict to main-container's height */,
height: '100%',
width: '100%',
maxWidth: '100%',
position: 'relative',
@ -695,8 +698,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
ref={scrollRef}
sx={{
position: 'relative',
minHeight: 0 /* Prevent flex overflow */,
// maxHeight: 'min-content',
minHeight: 0,
width: '100%',
display: 'flex',
flexGrow: 1,