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 { LoginRestricted } from 'components/ui/LoginRestricted';
import { ResumeGenerator } from 'components/ResumeGenerator'; import { ResumeGenerator } from 'components/ResumeGenerator';
import { JobsView } from 'components/ui/JobsView'; 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 { function WorkAddIcon(): JSX.Element {
return ( return (
@ -91,111 +91,131 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [jobTab, setJobTab] = useState<string>('select'); const [jobTab, setJobTab] = useState<string>('select');
const [analysisState, setAnalysisState] = useState<AnalysisState>({ const [analysisState, setAnalysisState] = useState<AnalysisState>(initialState);
...initialState,
candidate: selectedCandidate,
job: selectedJob,
});
const [canAdvance, setCanAdvance] = useState<boolean>(false); const [canAdvance, setCanAdvance] = useState<boolean>(false);
const [isInitialized, setIsInitialized] = useState<boolean>(false);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const [activeStep, setActiveStep] = useState<number>(user === null ? 0 : 1); const [activeStep, setActiveStep] = useState<number>(user === null ? 0 : 1);
const maxStep = 4; const maxStep = 4;
// Initialize from URL params on first load only
useEffect(() => { useEffect(() => {
if ( let isMounted = true;
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]);
useEffect(() => { const initializeFromParams = async () => {
let routeCandidateId = candidateId || ''; try {
let routeJobId = jobId || ''; // Initialize candidate
const routeStepId = stepId ? `/${stepId}` : ''; let candidate = selectedCandidate;
if (candidateId && candidateId !== selectedCandidate?.id) {
if (routeCandidateId && routeCandidateId !== selectedCandidate?.id) { const fetchedCandidate = await apiClient.getCandidate(candidateId);
apiClient if (fetchedCandidate && isMounted) {
.getCandidate(routeCandidateId) candidate = fetchedCandidate;
.then((candidate: Candidate | null) => { setSelectedCandidate(fetchedCandidate);
if (candidate) { } else if (isMounted && !fetchedCandidate) {
setSelectedCandidate(candidate);
routeCandidateId = candidate.id || '';
} else {
setError('Candidate not found.'); setError('Candidate not found.');
} }
}) } else if (!candidate && user) {
.catch((err: Error) => { // Fallback to user's candidate if no candidate selected
console.error('Error fetching candidate:', err); const userCandidate = await apiClient.getCandidate(user.id || '');
setError('Failed to fetch candidate information.'); if (userCandidate && isMounted) {
}); candidate = userCandidate;
} else { setSelectedCandidate(userCandidate);
routeCandidateId = selectedCandidate?.id || ''; }
} }
if (routeJobId && routeJobId !== selectedJob?.id) { // Initialize job
apiClient let job = selectedJob;
.getJob(routeJobId) if (jobId && jobId !== selectedJob?.id) {
.then((job: Job | null) => { const fetchedJob = await apiClient.getJob(jobId);
if (job) { if (fetchedJob && isMounted) {
setSelectedJob(job); job = fetchedJob;
routeJobId = job.id || ''; setSelectedJob(fetchedJob);
} else { } else if (isMounted && !fetchedJob) {
setError('Job not found.'); 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]);
useEffect(() => { // Initialize step
if (selectedCandidate === null && user !== null) { const urlStep = stepId ? parseInt(stepId, 10) : undefined;
apiClient if (urlStep !== undefined && !isNaN(urlStep) && urlStep !== activeStep) {
.getCandidate(user.id || '') setActiveStep(urlStep);
.then((candidate: Candidate | null) => {
if (candidate) {
setSelectedCandidate(candidate);
} }
})
.catch((err: Error) => { // Set analysis state
console.error('Error fetching candidate:', err); if (isMounted) {
setError('Failed to fetch candidate information.'); setAnalysisState({
...initialState,
candidate,
job,
}); });
setIsInitialized(true);
} }
}, [user, apiClient, selectedCandidate, setSelectedCandidate]); } 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 (!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}`;
}
}
// 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( const getMissingStepRequirement = useCallback(
(step: number) => { (step: number) => {
switch (step) { switch (step) {
case 0 /* candidate selection */: case 0: // candidate selection
break; break;
case 1 /* job selection */: case 1: // job selection
if (!analysisState.candidate) { if (!analysisState.candidate) {
return 'candidate'; return 'candidate';
} }
break; break;
case 2 /* job analysis */: case 2: // job analysis
if (!analysisState.candidate) { if (!analysisState.candidate) {
return 'candidate'; return 'candidate';
} }
@ -203,7 +223,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
return 'job'; return 'job';
} }
break; break;
case 3 /* resume generation */: case 3: // resume generation
if (!analysisState.candidate) { if (!analysisState.candidate) {
return 'candidate'; return 'candidate';
} }
@ -220,37 +240,21 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
[analysisState] [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(() => { useEffect(() => {
if (activeStep === maxStep) { if (activeStep === maxStep) {
setCanAdvance(false); setCanAdvance(false);
return; return;
} }
const blocked = getMissingStepRequirement(activeStep + 1); const blocked = getMissingStepRequirement(activeStep + 1);
if (blocked) { setCanAdvance(!blocked);
setCanAdvance(false);
} else {
setCanAdvance(true);
}
if (scrollRef.current) { if (scrollRef.current) {
scrollRef.current.scrollTo({ scrollRef.current.scrollTo({
top: 0, top: 0,
behavior: 'smooth', behavior: 'smooth',
}); });
} }
}, [setCanAdvance, analysisState, activeStep, getMissingStepRequirement]); }, [analysisState, activeStep, getMissingStepRequirement]);
const handleNext = (): void => { const handleNext = (): void => {
if (activeStep === maxStep) { if (activeStep === maxStep) {
@ -267,7 +271,6 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
if (activeStep === 0) { if (activeStep === 0) {
return; return;
} }
setActiveStep(prevActiveStep => prevActiveStep - 1); setActiveStep(prevActiveStep => prevActiveStep - 1);
}; };
@ -281,14 +284,10 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
}; };
const onCandidateSelect = (candidate: Candidate): void => { const onCandidateSelect = (candidate: Candidate): void => {
analysisState.candidate = candidate;
setAnalysisState({ ...analysisState });
setSelectedCandidate(candidate); setSelectedCandidate(candidate);
}; };
const onJobsSelected = (job: Job): void => { const onJobsSelected = (job: Job): void => {
analysisState.job = job;
setAnalysisState({ ...analysisState });
setSelectedJob(job); setSelectedJob(job);
}; };
@ -307,7 +306,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
<Box <Box
sx={{ sx={{
width: '100%', width: '100%',
minHeight: 0 /* Prevent flex overflow */, minHeight: 0,
maxHeight: 'min-content', maxHeight: 'min-content',
position: 'relative', position: 'relative',
display: 'flex', display: 'flex',
@ -334,7 +333,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
sx={{ sx={{
display: 'flex', display: 'flex',
position: 'relative', position: 'relative',
minHeight: 0 /* Prevent flex overflow */, minHeight: 0,
maxHeight: 'min-content', maxHeight: 'min-content',
flexGrow: 1, flexGrow: 1,
}} }}
@ -369,9 +368,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
return; return;
} }
console.log('Analysis complete:', analysis); console.log('Analysis complete:', analysis);
setAnalysisState({ ...analysisState, analysis }); setAnalysisState(prev => ({ ...prev, analysis }));
}, },
[analysisState] [analysisState.analysis?.score]
); );
// Render function for the analysis step // Render function for the analysis step
@ -379,10 +378,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
if (!analysisState.job || !analysisState.candidate) { if (!analysisState.job || !analysisState.candidate) {
return ( return (
<Box> <Box>
{JSON.stringify({ Missing required data:
job: analysisState.job, {!analysisState.candidate && ' candidate'}
candidate: analysisState.candidate, {!analysisState.job && ' job'}
})}
</Box> </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 ( return (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
height: '100%' /* Restrict to main-container's height */, height: '100%',
width: '100%', width: '100%',
maxWidth: '100%', maxWidth: '100%',
position: 'relative', position: 'relative',
@ -695,8 +698,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = () => {
ref={scrollRef} ref={scrollRef}
sx={{ sx={{
position: 'relative', position: 'relative',
minHeight: 0 /* Prevent flex overflow */, minHeight: 0,
// maxHeight: 'min-content',
width: '100%', width: '100%',
display: 'flex', display: 'flex',
flexGrow: 1, flexGrow: 1,