Reworked initial state loading logic
This commit is contained in:
parent
89bcc1cb55
commit
cf5936730c
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user