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 { 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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user