Fixed all eslint and prettier issues

This commit is contained in:
James Ketr 2025-06-20 13:56:07 -07:00
parent 2b1fbf2eaf
commit ff3e4605a1
33 changed files with 712 additions and 379 deletions

View File

@ -1,4 +1,6 @@
node_modules
dist
build
coverage
coverage
src/types/*
src/services/*

View File

@ -49,6 +49,7 @@
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"source-map-explorer": "^2.5.3",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
@ -7664,6 +7665,17 @@
"node-int64": "^0.4.0"
}
},
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -21945,6 +21957,95 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-explorer": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/source-map-explorer/-/source-map-explorer-2.5.3.tgz",
"integrity": "sha512-qfUGs7UHsOBE5p/lGfQdaAj/5U/GWYBw2imEpD6UQNkqElYonkow8t+HBL1qqIl3CuGZx7n8/CQo4x1HwSHhsg==",
"dependencies": {
"btoa": "^1.2.1",
"chalk": "^4.1.0",
"convert-source-map": "^1.7.0",
"ejs": "^3.1.5",
"escape-html": "^1.0.3",
"glob": "^7.1.6",
"gzip-size": "^6.0.0",
"lodash": "^4.17.20",
"open": "^7.3.1",
"source-map": "^0.7.4",
"temp": "^0.9.4",
"yargs": "^16.2.0"
},
"bin": {
"sme": "bin/cli.js",
"source-map-explorer": "bin/cli.js"
},
"engines": {
"node": ">=12"
}
},
"node_modules/source-map-explorer/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/source-map-explorer/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/source-map-explorer/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/source-map-explorer/node_modules/open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/source-map-explorer/node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"engines": {
"node": ">= 8"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -22967,6 +23068,18 @@
"node": ">=6"
}
},
"node_modules/temp": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dependencies": {
"mkdirp": "^0.5.1",
"rimraf": "~2.6.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/temp-dir": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz",
@ -22975,6 +23088,58 @@
"node": ">=8"
}
},
"node_modules/temp/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/temp/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/temp/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/temp/node_modules/rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/tempy": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz",

View File

@ -44,10 +44,12 @@
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"source-map-explorer": "^2.5.3",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "WDS_SOCKET_HOST=backstory-beta.ketrenos.com WDS_SOCKET_PORT=443 craco start",
"build": "craco build",
"test": "craco test",

View File

@ -135,7 +135,7 @@ const ResumeGenerator: React.FC<ResumeGeneratorProps> = (props: ResumeGeneratorP
console.error('Error saving resume:', error);
setSnack('Error saving resume.');
}
}, [apiClient, candidate.id, job.id, resume, setSnack]);
}, [apiClient, candidate.id, job.id, resume, setSnack, navigate, prompt, systemPrompt]);
return (
<Box

View File

@ -40,7 +40,7 @@ const BackstoryPageContainer = (props: BackstoryPageContainerProps): JSX.Element
p: '0 !important',
m: '0 auto !important',
// minWidth: variant === 'normal' ? '1024px' : '100%',
maxWidth: variant === 'normal' ? '1024px' : '100%',
maxWidth: variant === 'normal' ? '1024px' : '100% !important',
height: '100%',
minHeight: 0,
...sx,
@ -116,7 +116,10 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
.filter(Boolean);
};
const findMatchingRoute = (currentPath: string, routes: any[]) => {
const findMatchingRoute = (
currentPath: string,
routes: NavigationItem[]
): null | NavigationItem => {
for (const route of routes) {
if (!route.path) continue;
@ -124,7 +127,7 @@ const BackstoryLayout: React.FC<BackstoryLayoutProps> = (props: BackstoryLayoutP
const match = matchPath(
{
path: route.path,
exact: true,
end: true,
caseSensitive: false,
},
currentPath

View File

@ -1,5 +1,5 @@
// components/layout/Header.tsx
import React, { useState } from 'react';
import React, { JSX, useState } from 'react';
import { NavigateFunction, useLocation } from 'react-router-dom';
import {
AppBar,
@ -113,6 +113,14 @@ interface HeaderProps {
sessionId?: string | null;
}
type MenuItem = {
id: string;
label: string;
icon: React.ReactElement | null;
action: () => void;
group?: string;
};
const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const { user, logout } = useAuth();
const { setSnack } = useAppState();
@ -143,14 +151,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
const userMenuGroups = getUserMenuItemsByGroup(user?.userType || null, isAdmin);
// Create user menu items array with proper actions
const createUserMenuItems = () => {
const items: Array<{
id: string;
label: string;
icon: React.ReactElement | null;
action: () => void;
group?: string;
}> = [];
const createUserMenuItems = (): MenuItem[] => {
const items: Array<MenuItem> = [];
// Add profile group items
userMenuGroups.profile.forEach(item => {
@ -294,28 +296,28 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
};
// Desktop dropdown handlers
const handleDropdownOpen = (event: React.MouseEvent<HTMLElement>, itemId: string) => {
const handleDropdownOpen = (event: React.MouseEvent<HTMLElement>, itemId: string): void => {
setDropdownAnchors(prev => ({ ...prev, [itemId]: event.currentTarget }));
};
const handleDropdownClose = (itemId: string) => {
const handleDropdownClose = (itemId: string): void => {
setDropdownAnchors(prev => ({ ...prev, [itemId]: null }));
};
// Mobile accordion handlers
const handleMobileToggle = (itemId: string) => {
const handleMobileToggle = (itemId: string): void => {
setMobileExpanded(prev => ({ ...prev, [itemId]: !prev[itemId] }));
};
const handleDrawerToggle = () => {
const handleDrawerToggle = (): void => {
setMobileOpen(!mobileOpen);
};
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>): void => {
setUserMenuAnchor(event.currentTarget);
};
const handleUserMenuClose = () => {
const handleUserMenuClose = (): void => {
setUserMenuAnchor(null);
};
@ -325,7 +327,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
icon: React.ReactElement | null;
action: () => void;
group?: string;
}) => {
}): void => {
if (item.group !== 'divider') {
item.action();
handleUserMenuClose();
@ -333,7 +335,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
};
// Navigation handlers
const handleNavigate = (path: string) => {
const handleNavigate = (path: string): void => {
navigate(path.replace(/:.*$/, ''));
setMobileOpen(false);
// Close all dropdowns
@ -341,7 +343,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
};
// Render desktop navigation with dropdowns
const renderDesktopNavigation = () => {
const renderDesktopNavigation = (): JSX.Element => {
return (
<Box
sx={{
@ -364,7 +366,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
}}
>
<DropdownButton
onClick={e => handleDropdownOpen(e, item.id)}
onClick={(e): void => handleDropdownOpen(e, item.id)}
endIcon={<KeyboardArrowDown />}
sx={{
backgroundColor: isActive ? 'action.selected' : 'transparent',
@ -377,7 +379,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Menu
anchorEl={dropdownAnchors[item.id]}
open={Boolean(dropdownAnchors[item.id])}
onClose={() => handleDropdownClose(item.id)}
onClose={(): void => handleDropdownClose(item.id)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
TransitionComponent={Fade}
@ -385,7 +387,9 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
{item.children?.map(child => (
<MenuItem
key={child.id}
onClick={() => child.path && handleNavigate(child.path)}
onClick={(): void => {
child.path && handleNavigate(child.path);
}}
selected={isCurrentPath(child)}
disabled={!child.path}
sx={{
@ -406,7 +410,9 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
return (
<DropdownButton
key={item.id}
onClick={() => item.path && handleNavigate(item.path)}
onClick={(): void => {
item.path && handleNavigate(item.path);
}}
sx={{
backgroundColor: isActive ? 'action.selected' : 'transparent',
color: isActive ? 'secondary.main' : 'primary.contrastText',
@ -424,8 +430,8 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
};
// Render mobile accordion navigation
const renderMobileNavigation = () => {
const renderNavigationItem = (item: NavigationItem, depth = 0) => {
const renderMobileNavigation = (): JSX.Element => {
const renderNavigationItem = (item: NavigationItem, depth = 0): JSX.Element => {
const hasChildren = item.children && item.children.length > 0;
const isActive = isCurrentPath(item) || hasActiveChild(item);
const isExpanded = mobileExpanded[item.id];
@ -434,7 +440,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Box key={item.id}>
<ListItem disablePadding sx={{ pl: depth * 2 }}>
<ListItemButton
onClick={() => {
onClick={(): void => {
if (hasChildren) {
handleMobileToggle(item.id);
} else if (item.path) {
@ -485,7 +491,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Divider sx={{ my: 1 }} />
{!user && (
<ListItem disablePadding>
<ListItemButton onClick={() => handleNavigate('/login')}>
<ListItemButton onClick={(): void => handleNavigate('/login')}>
<ListItemText primary="Login" />
</ListItemButton>
</ListItem>
@ -495,7 +501,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
};
// Render user menu content
const renderUserMenu = () => {
const renderUserMenu = (): JSX.Element => {
return (
<UserMenuContainer>
<List dense>
@ -504,7 +510,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
<Divider key={`divider-${index}`} />
) : (
<ListItem key={item.id}>
<ListItemButton onClick={() => handleUserMenuAction(item)}>
<ListItemButton onClick={(): void => handleUserMenuAction(item)}>
{item.icon && <ListItemIcon sx={{ minWidth: 36 }}>{item.icon}</ListItemIcon>}
<ListItemText primary={item.label} />
</ListItemButton>
@ -517,13 +523,15 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
};
// Render user account section
const renderUserSection = () => {
const renderUserSection = (): JSX.Element => {
if (!user) {
return (
<Button
color="info"
variant="contained"
onClick={() => navigate('/login')}
onClick={(): void => {
navigate('/login');
}}
sx={{
display: { xs: 'none', sm: 'block' },
color: theme.palette.primary.contrastText,
@ -619,7 +627,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
'&:hover': { bgcolor: 'action.hover', opacity: 1 },
}}
content={`${window.location.origin}${window.location.pathname}?id=${sessionId}`}
onClick={() => {
onClick={(): void => {
navigate(`${window.location.pathname}?id=${sessionId}`);
setSnack('Link copied!');
}}
@ -644,7 +652,7 @@ const Header: React.FC<HeaderProps> = (props: HeaderProps) => {
</Container>
<Beta
sx={{ left: '-90px', '& .mobile': { right: '-72px' } }}
onClick={() => {
onClick={(): void => {
navigate('/docs/beta');
}}
/>

View File

@ -1,10 +1,10 @@
import React from 'react';
import React, { JSX } from 'react';
import { Typography, Avatar } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import 'components/layout/Header.css';
const BackstoryLogo = () => {
const BackstoryLogo = (): JSX.Element => {
const theme = useTheme();
return (
<Typography

View File

@ -41,7 +41,7 @@ const Beta: React.FC<BetaProps> = (props: BetaProps) => {
<Box
sx={sx}
className={`beta-clipper ${adaptive && isMobile && 'mobile'}`}
onClick={e => {
onClick={(e): void => {
onClick && onClick(e);
}}
>

View File

@ -40,7 +40,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
}
}, [candidate.description]);
const deleteCandidate = async (candidateId: string | undefined) => {
const deleteCandidate = async (candidateId: string | undefined): Promise<void> => {
if (candidateId) {
await apiClient.deleteCandidate(candidateId);
}
@ -116,7 +116,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
<Box sx={{ fontSize: '0.75rem', alignItems: 'center' }}>
<Link href={`/u/${candidate.username}`}>{`/u/${candidate.username}`}</Link>
<CopyBubble
onClick={(event: any) => {
onClick={(event): void => {
event.stopPropagation();
}}
tooltip="Copy link"
@ -151,7 +151,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
<Link
component="button"
variant="body2"
onClick={e => {
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
setIsDescriptionExpanded(!isDescriptionExpanded);
@ -209,7 +209,7 @@ const CandidateInfo: React.FC<CandidateInfoProps> = (props: CandidateInfoProps)
<Tooltip title="Delete Job">
<IconButton
size="small"
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
deleteCandidate(candidate.id);
}}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { JSX, useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import { BackstoryElementProps } from 'components/BackstoryTab';
@ -12,7 +12,7 @@ interface CandidatePickerProps extends BackstoryElementProps {
onSelect?: (candidate: Candidate) => void;
}
const CandidatePicker = (props: CandidatePickerProps) => {
const CandidatePicker = (props: CandidatePickerProps): JSX.Element => {
const { onSelect, sx } = props;
const { apiClient } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
@ -23,7 +23,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
if (candidates !== null) {
return;
}
const getCandidates = async () => {
const getCandidates = async (): Promise<void> => {
try {
const results = await apiClient.getCandidates();
const candidates: Candidate[] = results.data;
@ -49,7 +49,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
};
getCandidates();
}, [candidates, setSnack]);
}, [candidates, setSnack, apiClient]);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', ...sx }}>
@ -64,7 +64,7 @@ const CandidatePicker = (props: CandidatePickerProps) => {
{candidates?.map(u => (
<Paper
key={`${u.username}`}
onClick={() => {
onClick={(): void => {
onSelect ? onSelect(u) : setSelectedCandidate(u);
}}
sx={{ cursor: 'pointer' }}

View File

@ -67,13 +67,13 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
}
}, [job.summary]);
const deleteJob = async (jobId: string | undefined) => {
const deleteJob = async (jobId: string | undefined): Promise<void> => {
if (jobId) {
await apiClient.deleteJob(jobId);
}
};
const handleReset = async () => {
const handleReset = async (): Promise<void> => {
setActiveJob({ ...job });
};
@ -81,7 +81,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
return <Box>No job provided.</Box>;
}
const handleSave = async () => {
const handleSave = async (): Promise<void> => {
const newJob = await apiClient.updateJob(job.id || '', {
description: activeJob.description,
requirements: activeJob.requirements,
@ -91,15 +91,15 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
setSnack('Job updated.');
};
const handleRefresh = () => {
const handleRefresh = (): void => {
setAdminStatus('Re-extracting Job information...');
const jobStatusHandlers = {
onStatus: (status: Types.ChatMessageStatus) => {
onStatus: (status: Types.ChatMessageStatus): void => {
console.log('status:', status.content);
setAdminStatusType(status.activity);
setAdminStatus(status.content);
},
onMessage: async (jobMessage: Types.JobRequirementsMessage) => {
onMessage: async (jobMessage: Types.JobRequirementsMessage): Promise<void> => {
const newJob: Types.Job = jobMessage.job;
console.log('onMessage - job', newJob);
newJob.id = job.id;
@ -107,12 +107,12 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
const updatedJob: Types.Job = await apiClient.updateJob(job.id || '', newJob);
setActiveJob(updatedJob);
},
onError: (error: Types.ChatMessageError) => {
onError: (error: Types.ChatMessageError): void => {
console.log('onError', error);
setAdminStatusType(null);
setAdminStatus(null);
},
onComplete: () => {
onComplete: (): void => {
setAdminStatusType(null);
setAdminStatus(null);
},
@ -125,8 +125,8 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
items: string[] | undefined,
icon: JSX.Element,
required = false
) => {
if (!items || items.length === 0) return null;
): JSX.Element => {
if (!items || items.length === 0) return <></>;
return (
<Box sx={{ mb: 2 }}>
@ -162,8 +162,8 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
);
};
const renderJobRequirements = () => {
if (!activeJob.requirements) return null;
const renderJobRequirements = (): JSX.Element => {
if (!activeJob.requirements) return <></>;
return (
<Card elevation={0} sx={{ m: 0, p: 0, mt: 2, background: 'transparent !important' }}>
@ -320,7 +320,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Link
component="button"
variant="body2"
onClick={e => {
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
setIsDescriptionExpanded(!isDescriptionExpanded);
@ -402,7 +402,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Save Job">
<IconButton
size="small"
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
handleSave();
}}
@ -414,7 +414,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Delete Job">
<IconButton
size="small"
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
deleteJob(job.id);
setDeleted(true);
@ -426,7 +426,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Reset Job">
<IconButton
size="small"
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
handleReset();
}}
@ -437,7 +437,7 @@ const JobInfo: React.FC<JobInfoProps> = (props: JobInfoProps) => {
<Tooltip title="Reprocess Job">
<IconButton
size="small"
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
handleRefresh();
}}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { JSX, useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import { BackstoryElementProps } from 'components/BackstoryTab';
@ -12,7 +12,7 @@ interface JobPickerProps extends BackstoryElementProps {
onSelect?: (job: Job) => void;
}
const JobPicker = (props: JobPickerProps) => {
const JobPicker = (props: JobPickerProps): JSX.Element => {
const { onSelect } = props;
const { apiClient } = useAuth();
const { selectedJob } = useSelectedJob();
@ -23,7 +23,7 @@ const JobPicker = (props: JobPickerProps) => {
if (jobs !== null) {
return;
}
const getJobs = async () => {
const getJobs = async (): Promise<void> => {
try {
const results = await apiClient.getJobs();
const jobs: Job[] = results.data;
@ -41,7 +41,7 @@ const JobPicker = (props: JobPickerProps) => {
};
getJobs();
}, [jobs, setSnack]);
}, [jobs, setSnack, apiClient]);
return (
<Box sx={{ display: 'flex', flexDirection: 'column', mb: 1 }}>
@ -56,7 +56,7 @@ const JobPicker = (props: JobPickerProps) => {
{jobs?.map(j => (
<Paper
key={`${j.id}`}
onClick={() => {
onClick={(): void => {
console.log('Selected job', j);
onSelect && onSelect(j);
}}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { JSX, useEffect, useState } from 'react';
import {
Box,
Paper,
@ -71,7 +71,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
useEffect(() => {
if (loading) return; // Prevent multiple calls
const getJobs = async () => {
const getJobs = async (): Promise<void> => {
try {
const results = await apiClient.getJobs();
const jobsData: Job[] = results.data || [];
@ -102,12 +102,22 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
setLoading(true);
getJobs();
}, [apiClient, setSnack, loading]);
}, [
apiClient,
setSnack,
loading,
jobId,
selectedJob,
onSelect,
sortField,
sortOrder,
setSelectedJob,
]);
const sortJobs = (jobsList: Job[], field: SortField, order: SortOrder): Job[] => {
return [...jobsList].sort((a, b) => {
let aValue: any;
let bValue: any;
let aValue: number | string;
let bValue: number | string;
switch (field) {
case 'updatedAt':
@ -136,7 +146,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
});
};
const handleSort = (field: SortField) => {
const handleSort = (field: SortField): void => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
@ -145,20 +155,20 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
}
};
const handleJobSelect = (job: Job) => {
const handleJobSelect = (job: Job): void => {
setSelectedJob(job);
onSelect?.(job);
setMobileDialogOpen(true);
navigate(`/candidate/jobs/${job.id}`);
};
const handleMobileDialogClose = () => {
const handleMobileDialogClose = (): void => {
setMobileDialogOpen(false);
};
const sortedJobs = sortJobs(jobs, sortField, sortOrder);
const formatDate = (date: Date | undefined) => {
const formatDate = (date: Date | undefined): string => {
if (!date) return 'N/A';
return new Intl.DateTimeFormat('en-US', {
month: 'short',
@ -168,8 +178,8 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
}).format(date);
};
const getSortIcon = (field: SortField) => {
if (sortField !== field) return null;
const getSortIcon = (field: SortField): JSX.Element => {
if (sortField !== field) return <></>;
return sortOrder === 'asc' ? (
<ArrowUpIcon fontSize="small" />
) : (
@ -177,7 +187,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
);
};
const JobList = () => (
const JobList = (): JSX.Element => (
<Paper
elevation={isMobile ? 0 : 1}
sx={{
@ -209,7 +219,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
<Select
value={`${sortField}-${sortOrder}`}
label="Sort by"
onChange={e => {
onChange={(e): void => {
const [field, order] = e.target.value.split('-') as [SortField, SortOrder];
setSortField(field);
setSortOrder(order);
@ -248,7 +258,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
width: isMobile ? '25%' : 'auto',
backgroundColor: 'background.paper',
}}
onClick={() => handleSort('company')}
onClick={(): void => handleSort('company')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<BusinessIcon fontSize={isMobile ? 'small' : 'medium'} />
@ -267,7 +277,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
width: isMobile ? '45%' : 'auto',
backgroundColor: 'background.paper',
}}
onClick={() => handleSort('title')}
onClick={(): void => handleSort('title')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<WorkIcon fontSize={isMobile ? 'small' : 'medium'} />
@ -286,7 +296,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
px: 1,
backgroundColor: 'background.paper',
}}
onClick={() => handleSort('updatedAt')}
onClick={(): void => handleSort('updatedAt')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<ScheduleIcon fontSize="medium" />
@ -317,7 +327,9 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
key={job.id}
hover
selected={selectedJob?.id === job.id}
onClick={() => handleJobSelect(job)}
onClick={(): void => {
handleJobSelect(job);
}}
sx={{
cursor: 'pointer',
height: isMobile ? 48 : 'auto',
@ -431,7 +443,7 @@ const JobViewer: React.FC<JobViewerProps> = ({ onSelect }) => {
</Paper>
);
const JobDetails = ({ inDialog = false }: { inDialog?: boolean }) => (
const JobDetails = ({ inDialog = false }: { inDialog?: boolean }): JSX.Element => (
<Box
sx={{
flex: 1,

View File

@ -87,16 +87,16 @@ const JobsTable: React.FC<JobsTableProps> = ({
setLoading(false);
}
},
[limit]
[limit, apiClient]
);
// Initial load
React.useEffect(() => {
fetchJobs(0, searchQuery);
}, [fetchJobs]);
}, [fetchJobs, searchQuery]);
// Handle search with debouncing
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const value = event.target.value;
setSearchQuery(value);
@ -113,13 +113,13 @@ const JobsTable: React.FC<JobsTableProps> = ({
};
// Handle page change
const handlePageChange = (event: unknown, newPage: number) => {
const handlePageChange = (event: unknown, newPage: number): void => {
setPage(newPage);
fetchJobs(newPage, searchQuery);
};
// Handle rows per page change
const handleRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleRowsPerPageChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const newLimit = parseInt(event.target.value, 10);
setLimit(newLimit);
setPage(0);
@ -127,7 +127,7 @@ const JobsTable: React.FC<JobsTableProps> = ({
};
// Handle selection
const handleSelectAll = (event: React.ChangeEvent<HTMLInputElement>) => {
const handleSelectAll = (event: React.ChangeEvent<HTMLInputElement>): void => {
if (event.target.checked) {
const newSelected = new Set<string>(jobs.map(job => job.id || ''));
setSelectedJobs(newSelected);
@ -138,7 +138,7 @@ const JobsTable: React.FC<JobsTableProps> = ({
}
};
const handleSelectJob = (jobId: string) => {
const handleSelectJob = (jobId: string): void => {
const newSelected = new Set(selectedJobs);
if (newSelected.has(jobId)) {
newSelected.delete(jobId);
@ -151,17 +151,17 @@ const JobsTable: React.FC<JobsTableProps> = ({
onJobSelect?.(selectedJobsList);
};
const getOwnerName = (owner?: Types.Job['owner']) => {
const getOwnerName = (owner?: Types.Job['owner']): string => {
if (!owner) return 'Unknown';
return `${owner.firstName || ''} ${owner.lastName || ''}`.trim() || owner.email || 'Unknown';
};
const truncateDescription = (description: string, maxLength = 100) => {
const truncateDescription = (description: string, maxLength = 100): string => {
if (description.length <= maxLength) return description;
return description.substring(0, maxLength) + '...';
};
const isSelected = (jobId: string) => selectedJobs.has(jobId);
const isSelected = (jobId: string): boolean => selectedJobs.has(jobId);
const numSelected = selectedJobs.size;
const rowCount = jobs.length;
@ -262,7 +262,7 @@ const JobsTable: React.FC<JobsTableProps> = ({
<Checkbox
color="primary"
checked={isItemSelected}
onChange={() => handleSelectJob(job.id || '')}
onChange={(): void => handleSelectJob(job.id || '')}
inputProps={{
'aria-labelledby': `job-${job.id}`,
}}
@ -312,21 +312,21 @@ const JobsTable: React.FC<JobsTableProps> = ({
<Box sx={{ display: 'flex', gap: 0.5 }}>
{onJobView && (
<Tooltip title="View Job">
<IconButton size="small" onClick={() => onJobView(job)}>
<IconButton size="small" onClick={(): void => onJobView(job)}>
<VisibilityIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
{onJobEdit && (
<Tooltip title="Edit Job">
<IconButton size="small" onClick={() => onJobEdit(job)}>
<IconButton size="small" onClick={(): void => onJobEdit(job)}>
<EditIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
{onJobDelete && (
<Tooltip title="Delete Job">
<IconButton size="small" onClick={() => onJobDelete(job)}>
<IconButton size="small" onClick={(): void => onJobDelete(job)}>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>

View File

@ -1,11 +1,11 @@
import React from 'react';
import React, { JSX } from 'react';
import { Button, Typography, Paper, Container } from '@mui/material';
import { useNavigate } from 'react-router-dom';
interface LoginRequiredProps {
asset: string;
}
const LoginRequired = (props: LoginRequiredProps) => {
const LoginRequired = (props: LoginRequiredProps): JSX.Element => {
const { asset } = props;
const navigate = useNavigate();
@ -17,7 +17,7 @@ const LoginRequired = (props: LoginRequiredProps) => {
</Typography>
<Button
variant="contained"
onClick={() => {
onClick={(): void => {
navigate('/login');
}}
color="primary"

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { JSX } from 'react';
import Box from '@mui/material/Box';
import './LoginRestricted.css';
@ -6,7 +6,7 @@ interface LoginRestrictedProps {
children?: React.ReactNode;
}
const LoginRestricted = (props: LoginRestrictedProps) => {
const LoginRestricted = (props: LoginRestrictedProps): JSX.Element => {
const { children } = props;
return (
<Box className="LoginRestricted">

View File

@ -95,7 +95,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
}, [resume, activeResume]);
// Check if content needs truncation
const deleteResume = async (id: string | undefined) => {
const deleteResume = async (id: string | undefined): Promise<void> => {
if (id) {
try {
await apiClient.deleteResume(id);
@ -107,11 +107,11 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
}
};
const handleReset = async () => {
const handleReset = async (): Promise<void> => {
setActiveResume({ ...resume });
};
const handleSave = async () => {
const handleSave = async (): Promise<void> => {
setSaving(true);
try {
const resumeUpdate = {
@ -135,7 +135,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
}
};
const handleEditOpen = () => {
const handleEditOpen = (): void => {
setEditContent(activeResume.resume);
setEditSystemPrompt(activeResume.systemPrompt || '');
setEditPrompt(activeResume.prompt || '');
@ -146,7 +146,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
return <Box>No resume provided.</Box>;
}
const formatDate = (date: Date | undefined) => {
const formatDate = (date: Date | undefined): string => {
if (!date) return 'N/A';
try {
return new Intl.DateTimeFormat('en-US', {
@ -204,7 +204,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
await request.promise;
};
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
const handleTabChange = (event: React.SyntheticEvent, newValue: string): void => {
if (newValue === 'print') {
reactToPrintFn();
return;
@ -384,7 +384,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Tooltip title="Edit Resume">
<IconButton
size="small"
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
handleEditOpen();
}}
@ -396,7 +396,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Tooltip title="Delete Resume">
<IconButton
size="small"
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
deleteResume(activeResume.id);
}}
@ -408,7 +408,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
<Tooltip title="Reset Resume">
<IconButton
size="small"
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
handleReset();
}}
@ -432,7 +432,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{/* Edit Dialog */}
<Dialog
open={editDialogOpen}
onClose={() => {
onClose={(): void => {
setEditDialogOpen(false);
}}
maxWidth="lg"
@ -527,7 +527,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{tabValue === 'markdown' && (
<BackstoryTextField
value={editContent}
onChange={value => setEditContent(value)}
onChange={(value): void => setEditContent(value)}
style={{
position: 'relative',
maxHeight: '100%',
@ -546,7 +546,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{tabValue === 'systemPrompt' && (
<BackstoryTextField
value={editSystemPrompt}
onChange={value => setEditSystemPrompt(value)}
onChange={(value): void => setEditSystemPrompt(value)}
style={{
position: 'relative',
maxHeight: '100%',
@ -564,7 +564,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
{tabValue === 'prompt' && (
<BackstoryTextField
value={editPrompt}
onChange={value => setEditPrompt(value)}
onChange={(value): void => setEditPrompt(value)}
style={{
position: 'relative',
maxHeight: '100%',
@ -634,7 +634,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setEditDialogOpen(false)}>Cancel</Button>
<Button onClick={(): void => setEditDialogOpen(false)}>Cancel</Button>
<Button
onClick={handleSave}
variant="contained"

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { JSX, useEffect, useState } from 'react';
import {
Box,
Paper,
@ -76,7 +76,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
const { resumeId } = useParams<{ resumeId?: string }>();
useEffect(() => {
const getResumes = async () => {
const getResumes = async (): Promise<void> => {
try {
let results;
@ -114,7 +114,18 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
};
getResumes();
}, [apiClient, setSnack, candidateId, jobId]);
}, [
apiClient,
setSnack,
candidateId,
jobId,
resumeId,
selectedResume,
setSelectedResume,
onSelect,
sortField,
sortOrder,
]);
// Filter resumes based on search query
useEffect(() => {
@ -135,8 +146,8 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
const sortResumes = (resumesList: Resume[], field: SortField, order: SortOrder): Resume[] => {
return [...resumesList].sort((a, b) => {
let aValue: any;
let bValue: any;
let aValue: number | string;
let bValue: number | string;
switch (field) {
case 'updatedAt':
@ -165,7 +176,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
});
};
const handleSort = (field: SortField) => {
const handleSort = (field: SortField): void => {
if (sortField === field) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
@ -174,7 +185,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
}
};
const handleResumeSelect = (resume: Resume) => {
const handleResumeSelect = (resume: Resume): void => {
setSelectedResume(resume);
onSelect?.(resume);
if (isMobile) {
@ -184,17 +195,17 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
}
};
const handleMobileDialogClose = () => {
const handleMobileDialogClose = (): void => {
setMobileDialogOpen(false);
};
const handleSearchClear = () => {
const handleSearchClear = (): void => {
setSearchQuery('');
};
const sortedResumes = sortResumes(filteredResumes, sortField, sortOrder);
const formatDate = (date: Date | undefined) => {
const formatDate = (date: Date | undefined): string => {
if (!date) return 'N/A';
return new Intl.DateTimeFormat('en-US', {
month: 'short',
@ -204,8 +215,8 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
}).format(date);
};
const getSortIcon = (field: SortField) => {
if (sortField !== field) return null;
const getSortIcon = (field: SortField): JSX.Element => {
if (sortField !== field) return <></>;
return sortOrder === 'asc' ? (
<ArrowUpIcon fontSize="small" />
) : (
@ -213,13 +224,13 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
);
};
const getDisplayTitle = () => {
const getDisplayTitle = (): string => {
if (candidateId) return `Resumes for Candidate`;
if (jobId) return `Resumes for Job`;
return `All Resumes`;
};
const ResumeList = () => (
const ResumeList = (): JSX.Element => (
<Paper
elevation={isMobile ? 0 : 1}
sx={{
@ -262,7 +273,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
size="small"
placeholder="Search resumes..."
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
onChange={(e): void => setSearchQuery(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
@ -285,7 +296,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
<Select
value={`${sortField}-${sortOrder}`}
label="Sort by"
onChange={e => {
onChange={(e): void => {
const [field, order] = e.target.value.split('-') as [SortField, SortOrder];
setSortField(field);
setSortOrder(order);
@ -325,7 +336,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
width: isMobile ? '35%' : 'auto',
backgroundColor: 'background.paper',
}}
onClick={() => handleSort('candidateId')}
onClick={(): void => handleSort('candidateId')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<PersonIcon fontSize={isMobile ? 'small' : 'medium'} />
@ -344,7 +355,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
width: isMobile ? '35%' : 'auto',
backgroundColor: 'background.paper',
}}
onClick={() => handleSort('jobId')}
onClick={(): void => handleSort('jobId')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<WorkIcon fontSize={isMobile ? 'small' : 'medium'} />
@ -363,7 +374,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
px: 1,
backgroundColor: 'background.paper',
}}
onClick={() => handleSort('updatedAt')}
onClick={(): void => handleSort('updatedAt')}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<ScheduleIcon fontSize="medium" />
@ -394,7 +405,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
key={resume.id}
hover
selected={selectedResume?.id === resume.id}
onClick={() => handleResumeSelect(resume)}
onClick={(): void => handleResumeSelect(resume)}
sx={{
cursor: 'pointer',
height: isMobile ? 48 : 'auto',
@ -498,7 +509,7 @@ const ResumeViewer: React.FC<ResumeViewerProps> = ({ onSelect, candidateId, jobI
</Paper>
);
const ResumeDetails = ({ inDialog = false }: { inDialog?: boolean }) => (
const ResumeDetails = ({ inDialog = false }: { inDialog?: boolean }): JSX.Element => (
<Box
sx={{
flex: 1,

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { JSX } from 'react';
import {
SyncAlt,
Favorite,
@ -29,7 +29,7 @@ const StatusBox = styled(Box)(({ theme }) => ({
minHeight: 48,
}));
const StatusIcon = (props: StatusIconProps) => {
const StatusIcon = (props: StatusIconProps): JSX.Element => {
const { type } = props;
switch (type) {

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { JSX } from 'react';
import {
Chat as ChatIcon,
Dashboard as DashboardIcon,
@ -33,7 +33,7 @@ import { ResumeViewer } from 'components/ui/ResumeViewer';
import { JobsTable } from 'components/ui/JobsTable';
import * as Types from 'types/types';
const LogoutPage = () => {
const LogoutPage = (): JSX.Element => {
const { logout } = useAuth();
const navigate = useNavigate();
logout().then(() => {
@ -140,10 +140,10 @@ export const navigationConfig: NavigationConfig = {
icon: <WorkIcon />,
component: (
<JobsTable
onJobSelect={(selectedJobs: Types.Job[]) => console.log('Selected:', selectedJobs)}
onJobView={(job: Types.Job) => console.log('View job:', job)}
onJobEdit={(job: Types.Job) => console.log('Edit job:', job)}
onJobDelete={(job: Types.Job) => console.log('Delete job:', job)}
onJobSelect={(selectedJobs: Types.Job[]): void => console.log('Selected:', selectedJobs)}
onJobView={(job: Types.Job): void => console.log('View job:', job)}
onJobEdit={(job: Types.Job): void => console.log('Edit job:', job)}
onJobDelete={(job: Types.Job): void => console.log('Delete job:', job)}
selectable={true}
showActions={true}
/>

View File

@ -1,6 +1,14 @@
// Replace the existing AuthContext.tsx with these enhancements
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
useRef,
JSX,
} from 'react';
import * as Types from '../types/types';
import { ApiClient, CreateEmployerRequest, GuestConversionRequest } from 'services/api-client';
import { formatApiRequest, toCamelCase } from 'types/conversion';
@ -55,7 +63,11 @@ const TOKEN_STORAGE = {
// JWT Utilities
// ============================
function parseJwtPayload(token: string): any {
type JwtPayload = {
exp?: number; // Expiry time in seconds
};
function parseJwtPayload(token: string): JwtPayload | null {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
@ -181,11 +193,33 @@ function getStoredAuthData(): {
};
}
// ============================
// Enhanced Authentication Hook
// ============================
type EmailVerificationResponse = {
message: string;
userType: string;
};
function useAuthenticationLogic() {
interface AuthenticationLogic extends AuthState {
apiClient: ApiClient;
login: (loginData: LoginRequest) => Promise<boolean>;
logout: () => Promise<void>;
verifyMFA: (mfaData: Types.MFAVerifyRequest) => Promise<boolean>;
resendMFACode: (email: string, deviceId: string, deviceName: string) => Promise<boolean>;
clearMFA: () => void;
verifyEmail: (
verificationData: EmailVerificationRequest
) => Promise<EmailVerificationResponse | null>;
resendEmailVerification: (email: string) => Promise<boolean>;
setPendingVerificationEmail: (email: string) => void;
getPendingVerificationEmail: () => string | null;
createEmployerAccount: (employerData: CreateEmployerRequest) => Promise<boolean>;
requestPasswordReset: (email: string) => Promise<boolean>;
refreshAuth: () => Promise<boolean>;
updateUserData: (updatedUser: Types.User) => void;
convertGuestToUser: (registrationData: GuestConversionRequest) => Promise<boolean>;
createGuestSession: () => Promise<boolean>;
}
function useAuthenticationLogic(): AuthenticationLogic {
const [authState, setAuthState] = useState<AuthState>({
user: null,
guest: null,
@ -388,7 +422,7 @@ function useAuthenticationLogic() {
return;
}
const refreshTimer = setTimeout(() => {
const refreshTimer = setTimeout((): void => {
console.log('🔄 Auto-refreshing token before expiry...');
initializeAuth();
}, timeUntilExpiry);
@ -440,7 +474,7 @@ function useAuthenticationLogic() {
console.log('✅ Login successful, converted from guest to authenticated user');
return true;
}
} catch (error: any) {
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Network error occurred. Please try again.';
setAuthState(prev => ({
@ -483,7 +517,7 @@ function useAuthenticationLogic() {
console.log('✅ Guest successfully converted to permanent user');
return true;
} catch (error: any) {
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Failed to convert guest account';
setAuthState(prev => ({
@ -584,7 +618,9 @@ function useAuthenticationLogic() {
// Email verification functions (unchanged)
const verifyEmail = useCallback(
async (verificationData: EmailVerificationRequest) => {
async (
verificationData: EmailVerificationRequest
): Promise<EmailVerificationResponse | null> => {
setAuthState(prev => ({ ...prev, isLoading: true, error: null }));
try {
@ -715,7 +751,7 @@ function useAuthenticationLogic() {
await logout();
return false;
}
}, [refreshAccessToken, logout]);
}, [refreshAccessToken, logout, apiClient]);
// Resend MFA code
const resendMFACode = useCallback(
@ -781,13 +817,13 @@ function useAuthenticationLogic() {
const AuthContext = createContext<ReturnType<typeof useAuthenticationLogic> | null>(null);
function AuthProvider({ children }: { children: React.ReactNode }) {
function AuthProvider({ children }: { children: React.ReactNode }): JSX.Element {
const auth = useAuthenticationLogic();
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}
function useAuth() {
function useAuth(): ReturnType<typeof useAuthenticationLogic> {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
@ -811,7 +847,7 @@ function ProtectedRoute({
fallback = <div>Please log in to access this page.</div>,
requiredUserType,
allowGuests = false,
}: ProtectedRouteProps) {
}: ProtectedRouteProps): JSX.Element {
const { isAuthenticated, isInitializing, user, isGuest } = useAuth();
// Show loading while checking stored tokens

View File

@ -1,4 +1,12 @@
import React, { createContext, useContext, useState, useCallback, useEffect, useRef } from 'react';
import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
useRef,
JSX,
} from 'react';
import * as Types from 'types/types';
// Assuming you're using React Router
import { useLocation, useNavigate } from 'react-router-dom';
@ -27,6 +35,7 @@ const STORAGE_KEYS = {
interface RouteState {
lastRoute: string | null;
activeTab: string | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
appliedFilters: Record<string, any>;
sidebarCollapsed: boolean;
}
@ -59,6 +68,7 @@ export interface AppStateActions {
saveCurrentRoute: () => void;
restoreLastRoute: () => void;
setActiveTab: (tab: string) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setFilters: (filters: Record<string, any>) => void;
setSidebarCollapsed: (collapsed: boolean) => void;
clearRouteState: () => void;
@ -101,6 +111,7 @@ function getStoredObject<T>(key: string, defaultValue: T): T {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setStoredObject(key: string, value: any): void {
try {
localStorage.setItem(key, JSON.stringify(value));
@ -153,7 +164,7 @@ export function useAppStateLogic(): AppStateContextType {
// ============================
useEffect(() => {
const initializeFromStorage = async () => {
const initializeFromStorage = async (): Promise<void> => {
setIsInitializing(true);
try {
@ -167,7 +178,7 @@ export function useAppStateLogic(): AppStateContextType {
if (candidateId) {
promises.push(
(async () => {
(async (): Promise<void> => {
try {
const candidate = await apiClient.getCandidate(candidateId);
if (candidate) {
@ -187,7 +198,7 @@ export function useAppStateLogic(): AppStateContextType {
if (jobId) {
promises.push(
(async () => {
(async (): Promise<void> => {
try {
const job = await apiClient.getJob(jobId);
if (job) {
@ -207,7 +218,7 @@ export function useAppStateLogic(): AppStateContextType {
if (employerId) {
promises.push(
(async () => {
(async (): Promise<void> => {
try {
const employer = await apiClient.getEmployer(employerId);
if (employer) {
@ -235,7 +246,7 @@ export function useAppStateLogic(): AppStateContextType {
};
initializeFromStorage();
}, []);
}, [apiClient]);
// ============================
// Auto-save current route
@ -330,6 +341,7 @@ export function useAppStateLogic(): AppStateContextType {
});
}, []);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const setFilters = useCallback((filters: Record<string, any>) => {
setRouteStateState(prev => {
const newState = { ...prev, appliedFilters: filters };
@ -397,9 +409,9 @@ export function useAppStateLogic(): AppStateContextType {
const AppStateContext = createContext<AppStateContextType | null>(null);
export function AppStateProvider({ children }: { children: React.ReactNode }) {
export function AppStateProvider({ children }: { children: React.ReactNode }): JSX.Element {
const appState = useAppStateLogic();
const snackRef = useRef<any>(null);
const snackRef = useRef<{ setSnack: SetSnackType }>(null);
// Global UI components
appState.setSnack = useCallback(
@ -417,7 +429,7 @@ export function AppStateProvider({ children }: { children: React.ReactNode }) {
);
}
export function useAppState() {
export function useAppState(): AppStateContextType {
const context = useContext(AppStateContext);
if (!context) {
throw new Error('useAppState must be used within an AppStateProvider');
@ -429,27 +441,47 @@ export function useAppState() {
// Convenience Hooks
// ============================
export function useSelectedCandidate() {
export function useSelectedCandidate(): {
selectedCandidate: Types.Candidate | null;
setSelectedCandidate: (candidate: Types.Candidate | null) => void;
} {
const { selectedCandidate, setSelectedCandidate } = useAppState();
return { selectedCandidate, setSelectedCandidate };
}
export function useSelectedJob() {
export function useSelectedJob(): {
selectedJob: Types.Job | null;
setSelectedJob: (job: Types.Job | null) => void;
} {
const { selectedJob, setSelectedJob } = useAppState();
return { selectedJob, setSelectedJob };
}
export function useSelectedEmployer() {
export function useSelectedEmployer(): {
selectedEmployer: Types.Employer | null;
setSelectedEmployer: (employer: Types.Employer | null) => void;
} {
const { selectedEmployer, setSelectedEmployer } = useAppState();
return { selectedEmployer, setSelectedEmployer };
}
export const useSelectedResume = () => {
export const useSelectedResume = (): {
selectedResume: Types.Resume | null;
setSelectedResume: (resume: Types.Resume | null) => void;
} => {
const { selectedResume, setSelectedResume } = useAppState();
return { selectedResume, setSelectedResume };
};
export function useRouteState() {
export function useRouteState(): {
routeState: RouteState;
setActiveTab: (tab: string) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
setFilters: (filters: Record<string, any>) => void;
setSidebarCollapsed: (collapsed: boolean) => void;
restoreLastRoute: () => void;
clearRouteState: () => void;
} {
const {
routeState,
setActiveTab,
@ -469,7 +501,7 @@ export function useRouteState() {
};
}
export function useAppInitializing() {
export function useAppInitializing(): boolean {
const { isInitializing } = useAppState();
return isInitializing;
}

View File

@ -5,11 +5,15 @@ const debug = false;
type ResizeCallback = () => void;
// Define the debounce function with cancel capability
function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number
): T & { cancel: () => void } {
let timeout: NodeJS.Timeout | null = null;
let lastCall = 0;
const debounced = function (...args: Parameters<T>) {
const debounced = function (...args: Parameters<T>): void {
const now = Date.now();
// Execute immediately if wait time has passed since last call
@ -35,21 +39,21 @@ function debounce<T extends (...args: any[]) => void>(func: T, wait: number) {
};
// Add cancel method to clear pending timeout
debounced.cancel = function () {
debounced.cancel = function (): void {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
};
return debounced;
return debounced as T & { cancel: () => void };
}
const useResizeObserverAndMutationObserver = (
targetRef: RefObject<HTMLElement | null>,
scrollToRef: RefObject<HTMLElement | null> | null,
callback: ResizeCallback
) => {
): void => {
const callbackRef = useRef(callback);
const resizeObserverRef = useRef<ResizeObserver | null>(null);
const mutationObserverRef = useRef<MutationObserver | null>(null);
@ -68,10 +72,10 @@ const useResizeObserverAndMutationObserver = (
requestAnimationFrame(() => callbackRef.current());
}, 500);
const resizeObserver = new ResizeObserver((_e: any) => {
const resizeObserver = new ResizeObserver((): void => {
debouncedCallback('resize');
});
const mutationObserver = new MutationObserver((_e: any) => {
const mutationObserver = new MutationObserver((): void => {
debouncedCallback('mutation');
});
@ -92,7 +96,7 @@ const useResizeObserverAndMutationObserver = (
resizeObserverRef.current = resizeObserver;
mutationObserverRef.current = mutationObserver;
return () => {
return (): void => {
debouncedCallback.cancel();
resizeObserver.disconnect();
mutationObserver.disconnect();
@ -186,7 +190,7 @@ const useAutoScrollToBottom = (
const scrollTo = scrollToRef.current;
if (!container) return;
const handleScroll = (ev: Event, pause?: number) => {
const handleScroll = (ev: Event, pause?: number): void => {
const currentScrollTop = container.scrollTop;
/* If the user is scrolling up *or* they used the scroll wheel and didn't scroll,
* they may be zooming in a region; pause scrolling */
@ -205,17 +209,17 @@ const useAutoScrollToBottom = (
);
};
const pauseScroll = (ev: Event) => {
const pauseScroll = (ev: Event): void => {
debug && console.log('Pausing for mouse movement');
handleScroll(ev, 500);
};
const pauseClick = (ev: Event) => {
const pauseClick = (ev: Event): void => {
debug && console.log('Pausing for mouse click');
handleScroll(ev, 1000);
};
const handlePaste = () => {
const handlePaste = (): void => {
console.log('handlePaste');
// Delay scroll check to ensure DOM updates
setTimeout(() => {

View File

@ -82,7 +82,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
return () => clearTimeout(timer);
}, []);
const handleReturn = () => {
const handleReturn = (): void => {
if (onReturn) {
onReturn();
} else if (returnPath) {
@ -220,7 +220,7 @@ const BetaPage: React.FC<BetaPageProps> = ({
color: '#808080',
},
}}
onClick={() => {
onClick={(): void => {
navigate('/docs/beta');
}}
/>

View File

@ -1,4 +1,4 @@
import React, { forwardRef, useState, useEffect, useRef } from 'react';
import React, { forwardRef, useState, useEffect, useRef, JSX } from 'react';
import { Box, Paper, Button, Tooltip } from '@mui/material';
import { Send as SendIcon } from '@mui/icons-material';
import { useAuth } from 'hooks/AuthContext';
@ -9,6 +9,7 @@ import {
ChatMessageError,
ChatMessageStreaming,
ChatMessageStatus,
ChatMessageMetaData,
} from 'types/types';
import { ConversationHandle } from 'components/Conversation';
import { BackstoryPageProps } from 'components/BackstoryTab';
@ -22,6 +23,20 @@ import { BackstoryQuery } from 'components/BackstoryQuery';
import { CandidatePicker } from 'components/ui/CandidatePicker';
import { Scrollable } from 'components/Scrollable';
const emptyMetadata: ChatMessageMetaData = {
model: 'qwen2.5',
temperature: 0,
maxTokens: 0,
topP: 0,
frequencyPenalty: 0,
presencePenalty: 0,
stopSequences: [],
evalCount: 0,
evalDuration: 0,
promptEvalCount: 0,
promptEvalDuration: 0,
};
const defaultMessage: ChatMessage = {
status: 'done',
type: 'text',
@ -29,11 +44,11 @@ const defaultMessage: ChatMessage = {
timestamp: new Date(),
content: '',
role: 'user',
metadata: null as any,
metadata: emptyMetadata,
};
const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
(_props: BackstoryPageProps, ref) => {
(_props: BackstoryPageProps, ref): JSX.Element => {
const { apiClient } = useAuth();
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
const [processingMessage, setProcessingMessage] = useState<
@ -48,25 +63,9 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [streaming, setStreaming] = useState<boolean>(false);
const messagesEndRef = useRef(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Load messages for current session
const loadMessages = async () => {
if (!chatSession?.id) return;
try {
const result = await apiClient.getChatMessages(chatSession.id);
const chatMessages: ChatMessage[] = result.data;
setMessages(chatMessages);
setProcessingMessage(null);
setStreamingMessage(null);
console.log(`getChatMessages returned ${chatMessages.length} messages.`, chatMessages);
} catch (error) {
console.error('Failed to load messages:', error);
}
};
const onDelete = async (session: ChatSession) => {
const onDelete = async (session: ChatSession): Promise<void> => {
if (!session.id) {
return;
}
@ -82,7 +81,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
};
// Send message
const sendMessage = async (message: string) => {
const sendMessage = async (message: string): Promise<void> => {
if (!message.trim() || !chatSession?.id || streaming || !selectedCandidate) return;
const messageContent = message;
@ -105,21 +104,21 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
});
setMessages(prev => {
const filtered = prev.filter((m: any) => m.id !== chatMessage.id);
return [...filtered, chatMessage] as any;
const filtered = prev.filter(m => m.id !== chatMessage.id);
return [...filtered, chatMessage] as ChatMessage[];
});
try {
apiClient.sendMessageStream(chatMessage, {
onMessage: (msg: ChatMessage) => {
onMessage: (msg: ChatMessage): void => {
setMessages(prev => {
const filtered = prev.filter((m: any) => m.id !== msg.id);
return [...filtered, msg] as any;
const filtered = prev.filter(m => m.id !== msg.id);
return [...filtered, msg] as ChatMessage[];
});
setStreamingMessage(null);
setProcessingMessage(null);
},
onError: (error: string | ChatMessageError) => {
onError: (error: string | ChatMessageError): void => {
console.log('onError:', error);
// Type-guard to determine if this is a ChatMessageBase or a string
if (typeof error === 'object' && error !== null && 'content' in error) {
@ -133,18 +132,18 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
}
setStreaming(false);
},
onStreaming: (chunk: ChatMessageStreaming) => {
onStreaming: (chunk: ChatMessageStreaming): void => {
// console.log("onStreaming:", chunk);
setStreamingMessage({
...chunk,
role: 'assistant',
metadata: null as any,
metadata: emptyMetadata,
});
},
onStatus: (status: ChatMessageStatus) => {
onStatus: (status: ChatMessageStatus): void => {
setProcessingMessage(status);
},
onComplete: () => {
onComplete: (): void => {
console.log('onComplete');
setStreamingMessage(null);
setProcessingMessage(null);
@ -159,7 +158,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
// Auto-scroll to bottom when new messages arrive
useEffect(() => {
(messagesEndRef.current as any)?.scrollIntoView({ behavior: 'smooth' });
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// Load sessions when username changes
@ -182,14 +181,29 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
} finally {
setLoading(false);
}
}, [selectedCandidate]);
}, [selectedCandidate, apiClient, setSnack]);
// Load messages when session changes
useEffect(() => {
const loadMessages = async (): Promise<void> => {
if (!chatSession?.id) return;
try {
const result = await apiClient.getChatMessages(chatSession.id);
const chatMessages: ChatMessage[] = result.data;
setMessages(chatMessages);
setProcessingMessage(null);
setStreamingMessage(null);
console.log(`getChatMessages returned ${chatMessages.length} messages.`, chatMessages);
} catch (error) {
console.error('Failed to load messages:', error);
}
};
if (chatSession?.id) {
loadMessages();
}
}, [chatSession]);
}, [chatSession, apiClient]);
if (!selectedCandidate) {
return <CandidatePicker />;
@ -202,7 +216,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
status: 'done',
timestamp: new Date(),
content: `Welcome to the Backstory Chat about ${selectedCandidate.fullName}. Ask any questions you have about ${selectedCandidate.firstName}.`,
metadata: null as any,
metadata: emptyMetadata,
};
return (
@ -237,7 +251,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
/>
<Button
sx={{ maxWidth: 'max-content' }}
onClick={() => {
onClick={(): void => {
setSelectedCandidate(null);
}}
variant="contained"
@ -299,7 +313,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
{/* Fixed Message Input */}
<Box sx={{ display: 'flex', flexShrink: 1, gap: 1 }}>
<DeleteConfirmation
onDelete={() => {
onDelete={(): void => {
chatSession && onDelete(chatSession);
}}
disabled={!chatSession}
@ -325,7 +339,7 @@ const CandidateChatPage = forwardRef<ConversationHandle, BackstoryPageProps>(
>
<Button
variant="contained"
onClick={() => {
onClick={(): void => {
sendMessage(
(backstoryTextRef.current && backstoryTextRef.current.getAndResetValue()) || ''
);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef, useCallback } from 'react';
import React, { useEffect, useState, useRef, useCallback, JSX } from 'react';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Tooltip from '@mui/material/Tooltip';
@ -24,11 +24,26 @@ import {
CandidateAI,
ChatMessageStatus,
ChatMessageError,
ChatMessageMetaData,
} from 'types/types';
import { useAuth } from 'hooks/AuthContext';
import { Message } from 'components/Message';
import { useAppState } from 'hooks/GlobalContext';
const emptyMetadata: ChatMessageMetaData = {
model: 'qwen2.5',
temperature: 0,
maxTokens: 0,
topP: 0,
frequencyPenalty: 0,
presencePenalty: 0,
stopSequences: [],
evalCount: 0,
evalDuration: 0,
promptEvalCount: 0,
promptEvalDuration: 0,
};
const defaultMessage: ChatMessage = {
status: 'done',
type: 'text',
@ -36,10 +51,10 @@ const defaultMessage: ChatMessage = {
timestamp: new Date(),
content: '',
role: 'user',
metadata: null as any,
metadata: emptyMetadata,
};
const GenerateCandidate = (_props: BackstoryElementProps) => {
const GenerateCandidate = (_props: BackstoryElementProps): JSX.Element => {
const { apiClient, user } = useAuth();
const { setSnack } = useAppState();
const [processingMessage, setProcessingMessage] = useState<ChatMessage | null>(null);
@ -96,7 +111,7 @@ const GenerateCandidate = (_props: BackstoryElementProps) => {
return;
}
const generatePersona = async (prompt: string) => {
const generatePersona = async (prompt: string): Promise<void> => {
const userMessage: ChatMessageUser = {
type: 'text',
role: 'user',
@ -327,7 +342,7 @@ const GenerateCandidate = (_props: BackstoryElementProps) => {
}}
variant="contained"
disabled={processing || !canGenImage}
onClick={() => {
onClick={(): void => {
setShouldGenerateProfile(true);
}}
>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { JSX } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Box,
@ -197,12 +197,12 @@ interface HeroButtonProps extends ButtonProps {
children?: string;
path: string;
}
const HeroButton = (props: HeroButtonProps) => {
const HeroButton = (props: HeroButtonProps): JSX.Element => {
const { children, onClick, path, ...rest } = props;
const navigate = useNavigate();
const handleClick = () => {
const handleClick = (): void => {
navigate(path);
};
@ -227,7 +227,7 @@ const HeroButton = (props: HeroButtonProps) => {
const HowItWorks: React.FC = () => {
const navigate = useNavigate();
const handleGetStarted = () => {
const handleGetStarted = (): void => {
navigate('/job-analysis');
};
@ -260,7 +260,10 @@ const HowItWorks: React.FC = () => {
>
Your complete professional story, beyond a single page
</Typography>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 400, fontSize: { xs: '1rem', md: '1.25rem' } }}>
<Typography
variant="h5"
sx={{ mb: 3, fontWeight: 400, fontSize: { xs: '1rem', md: '1.25rem' } }}
>
Let potential employers discover the depth of your experience through interactive
Q&A and tailored resumes
</Typography>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import React, { useState, useEffect, useCallback, useRef, JSX } from 'react';
import {
Box,
Stepper,
@ -32,7 +32,7 @@ import { LoginRestricted } from 'components/ui/LoginRestricted';
import { ResumeGenerator } from 'components/ResumeGenerator';
import { JobInfo } from 'components/ui/JobInfo';
function WorkAddIcon() {
function WorkAddIcon(): JSX.Element {
return (
<Box
position="relative"
@ -99,7 +99,7 @@ const steps: Step[] = [
return { ...item, index, label: item.title.toLowerCase().replace(/ /g, '-') };
});
const capitalize = (str: string) => {
const capitalize = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1);
};
@ -123,6 +123,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
if (!analysisState) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const missing = step.requiredState.find(f => !(analysisState as any)[f]);
return missing;
},
@ -141,6 +142,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
};
setAnalysisState(analysis);
for (let i = steps.length - 1; i >= 0; i--) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const missing = steps[i].requiredState.find(f => !(analysis as any)[f]);
if (!missing) {
setActiveStep(steps[i]);
@ -166,16 +168,16 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
behavior: 'smooth',
});
}
}, [setCanAdvance, analysisState, activeStep]);
}, [setCanAdvance, analysisState, activeStep, canAccessStep]);
const handleNext = () => {
const handleNext = (): void => {
if (activeStep.index === steps.length - 1) {
return;
}
const missing = canAccessStep(steps[activeStep.index + 1]);
if (missing) {
setError(`${capitalize(missing)} is necessary before continuing.`);
return missing;
return;
}
if (activeStep.index < steps.length - 1) {
@ -183,7 +185,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
}
};
const handleBack = () => {
const handleBack = (): void => {
if (activeStep.index === 0) {
return;
}
@ -191,7 +193,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
setActiveStep(prevActiveStep => steps[prevActiveStep.index - 1]);
};
const moveToStep = (step: number) => {
const moveToStep = (step: number): void => {
const missing = canAccessStep(steps[step]);
if (missing) {
setError(`${capitalize(missing)} is needed to access this step.`);
@ -200,7 +202,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
setActiveStep(steps[step]);
};
const onCandidateSelect = (candidate: Candidate) => {
const onCandidateSelect = (candidate: Candidate): void => {
if (!analysisState) {
return;
}
@ -210,7 +212,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
handleNext();
};
const onJobSelect = (job: Job) => {
const onJobSelect = (job: Job): void => {
if (!analysisState) {
return;
}
@ -221,16 +223,16 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
};
// Render function for the candidate selection step
const renderCandidateSelection = () => (
const renderCandidateSelection = (): JSX.Element => (
<CandidatePicker sx={{ pt: 1 }} onSelect={onCandidateSelect} />
);
const handleTabChange = (event: React.SyntheticEvent, value: string) => {
const handleTabChange = (event: React.SyntheticEvent, value: string): void => {
setJobTab(value);
};
// Render function for the job description step
const renderJobDescription = () => {
const renderJobDescription = (): JSX.Element => {
return (
<Box sx={{ mt: 3, width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
@ -251,7 +253,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
);
};
const onAnalysisComplete = (skills: SkillAssessment[]) => {
const onAnalysisComplete = (skills: SkillAssessment[]): void => {
if (!analysisState) {
return;
}
@ -260,9 +262,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
};
// Render function for the analysis step
const renderAnalysis = () => {
const renderAnalysis = (): JSX.Element => {
if (!analysisState) {
return;
return <></>;
}
if (!analysisState.job || !analysisState.candidate) {
return (
@ -286,9 +288,9 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
);
};
const renderResume = () => {
const renderResume = (): JSX.Element => {
if (!analysisState) {
return;
return <></>;
}
if (!analysisState.job || !analysisState.candidate || !analysisState.analysis) {
return <></>;
@ -326,11 +328,11 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
<Step key={step.index}>
<StepLabel
sx={{ cursor: 'pointer' }}
onClick={() => {
onClick={(): void => {
moveToStep(index);
}}
slots={{
stepIcon: () => (
stepIcon: (): JSX.Element => (
<Avatar
key={step.index}
sx={{
@ -410,7 +412,7 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
{activeStep.index === steps[steps.length - 1].index ? (
<Button
disabled={!canAdvance}
onClick={() => {
onClick={(): void => {
moveToStep(0);
}}
variant="outlined"
@ -428,10 +430,10 @@ const JobAnalysisPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProp
<Snackbar
open={!!error}
autoHideDuration={6000}
onClose={() => setError(null)}
onClose={(): void => setError(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert onClose={() => setError(null)} severity="error" sx={{ width: '100%' }}>
<Alert onClose={(): void => setError(null)} severity="error" sx={{ width: '100%' }}>
{error}
</Alert>
</Snackbar>

View File

@ -53,7 +53,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProps) =>
setLoading(false);
}, 3000);
}
}, [error, loading]);
}, [error, loading, setSnack]);
useEffect(() => {
if (tab === 'register') {
@ -61,7 +61,7 @@ const LoginPage: React.FC<BackstoryPageProps> = (_props: BackstoryPageProps) =>
}
}, [tab, setTabValue]);
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
const handleTabChange = (event: React.SyntheticEvent, newValue: string): void => {
setTabValue(newValue);
setSuccess(null);
};

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { JSX } from 'react';
import { Box, Card, CardContent, Typography, Button, LinearProgress, Stack } from '@mui/material';
import {
Add as AddIcon,
@ -17,7 +17,7 @@ import { useAppState } from 'hooks/GlobalContext';
type CandidateDashboardProps = BackstoryElementProps;
const CandidateDashboard = (_props: CandidateDashboardProps) => {
const CandidateDashboard = (_props: CandidateDashboardProps): JSX.Element => {
const { setSnack } = useAppState();
const navigate = useNavigate();
const { user } = useAuth();
@ -69,7 +69,7 @@ const CandidateDashboard = (_props: CandidateDashboardProps) => {
variant="contained"
color="primary"
sx={{ mt: 1 }}
onClick={e => {
onClick={(e): void => {
e.stopPropagation();
navigate('/candidate/profile');
}}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, JSX } from 'react';
import {
Box,
Button,
@ -71,7 +71,7 @@ interface TabPanelProps {
value: number;
}
function TabPanel(props: TabPanelProps) {
function TabPanel(props: TabPanelProps): JSX.Element {
const { children, value, index, ...other } = props;
return (
@ -166,12 +166,12 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
}
// Handle tab change
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
const handleTabChange = (event: React.SyntheticEvent, newValue: number): void => {
setTabValue(newValue);
};
// Handle form input changes
const handleInputChange = (field: string, value: any) => {
const handleInputChange = (field: string, value: boolean | string): void => {
setFormData({
...formData,
[field]: value,
@ -179,7 +179,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
};
// Handle profile image upload
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
if (!e.target.files || !e.target.files[0]) {
return;
}
@ -210,7 +210,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
};
// Toggle edit mode for a section
const toggleEditMode = (section: string) => {
const toggleEditMode = (section: string): void => {
setEditMode({
...editMode,
[section]: !editMode[section],
@ -218,7 +218,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
};
// Save changes
const handleSave = async (section: string) => {
const handleSave = async (section: string): Promise<void> => {
setLoading(true);
try {
if (candidate.id) {
@ -239,13 +239,13 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
};
// Cancel edit
const handleCancel = (section: string) => {
const handleCancel = (section: string): void => {
setFormData(candidate);
toggleEditMode(section);
};
// Add new skill
const handleAddSkill = () => {
const handleAddSkill = (): void => {
if (newSkill.name && newSkill.category) {
const updatedSkills = [...(formData.skills || []), newSkill as Types.Skill];
setFormData({ ...formData, skills: updatedSkills });
@ -260,13 +260,13 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
};
// Remove skill
const handleRemoveSkill = (index: number) => {
const handleRemoveSkill = (index: number): void => {
const updatedSkills = (formData.skills || []).filter((_, i) => i !== index);
setFormData({ ...formData, skills: updatedSkills });
};
// Add new work experience
const handleAddExperience = () => {
const handleAddExperience = (): void => {
if (newExperience.companyName && newExperience.position) {
const updatedExperience = [
...(formData.experience || []),
@ -287,13 +287,13 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
};
// Remove work experience
const handleRemoveExperience = (index: number) => {
const handleRemoveExperience = (index: number): void => {
const updatedExperience = (formData.experience || []).filter((_, i) => i !== index);
setFormData({ ...formData, experience: updatedExperience });
};
// Basic Information Tab
const renderBasicInfo = () => (
const renderBasicInfo = (): JSX.Element => (
<Box
sx={{
display: 'flex',
@ -368,7 +368,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
control={
<Switch
checked={formData.isPublic}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onChange={(event: React.ChangeEvent<HTMLInputElement>): void =>
handleInputChange('isPublic', event.target.checked)
}
/>
@ -394,7 +394,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
fullWidth
label="First Name"
value={formData.firstName || ''}
onChange={e => handleInputChange('firstName', e.target.value)}
onChange={(e): void => handleInputChange('firstName', e.target.value)}
variant="outlined"
/>
) : (
@ -411,7 +411,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
fullWidth
label="Last Name"
value={formData.lastName || ''}
onChange={e => handleInputChange('lastName', e.target.value)}
onChange={(e): void => handleInputChange('lastName', e.target.value)}
variant="outlined"
/>
) : (
@ -436,7 +436,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
fullWidth
label="Phone"
value={formData.phone || ''}
onChange={e => handleInputChange('phone', e.target.value)}
onChange={(e): void => handleInputChange('phone', e.target.value)}
variant="outlined"
/>
) : (
@ -458,7 +458,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
rows={3}
label="Professional Summary"
value={formData.description || ''}
onChange={e => handleInputChange('description', e.target.value)}
onChange={(e): void => handleInputChange('description', e.target.value)}
variant="outlined"
/>
) : (
@ -493,7 +493,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
<>
<Button
variant="outlined"
onClick={() => handleCancel('basic')}
onClick={(): void => handleCancel('basic')}
startIcon={<Cancel />}
fullWidth={isMobile}
>
@ -501,7 +501,9 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
</Button>
<Button
variant="contained"
onClick={() => handleSave('basic')}
onClick={(): void => {
handleSave('basic');
}}
startIcon={loading ? <CircularProgress size={20} color="inherit" /> : <Save />}
disabled={loading}
fullWidth={isMobile}
@ -512,7 +514,9 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
) : (
<Button
variant="outlined"
onClick={() => toggleEditMode('basic')}
onClick={(): void => {
toggleEditMode('basic');
}}
startIcon={<Edit />}
fullWidth={isMobile}
>
@ -525,7 +529,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
);
// Skills Tab
const renderSkills = () => (
const renderSkills = (): JSX.Element => (
<Box>
<Box
sx={{
@ -541,7 +545,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
<Button
variant="outlined"
startIcon={<Add />}
onClick={() => setSkillDialog(true)}
onClick={(): void => setSkillDialog(true)}
fullWidth={isMobile}
size={isMobile ? 'small' : 'medium'}
>
@ -605,7 +609,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
</Box>
<IconButton
size="small"
onClick={() => handleRemoveSkill(index)}
onClick={(): void => handleRemoveSkill(index)}
color="error"
sx={{ ml: 1 }}
>
@ -627,7 +631,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
);
// Experience Tab
const renderExperience = () => (
const renderExperience = (): JSX.Element => (
<Box>
<Box
sx={{
@ -643,7 +647,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
<Button
variant="outlined"
startIcon={<Add />}
onClick={() => setExperienceDialog(true)}
onClick={(): void => setExperienceDialog(true)}
fullWidth={isMobile}
size={isMobile ? 'small' : 'medium'}
>
@ -721,7 +725,9 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
)}
</Box>
<IconButton
onClick={() => handleRemoveExperience(index)}
onClick={(): void => {
handleRemoveExperience(index);
}}
color="error"
size="small"
sx={{
@ -752,7 +758,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
</Box>
);
const renderEducation = () => (
const renderEducation = (): JSX.Element => (
<Box>
<Box
sx={{
@ -868,7 +874,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
{/* Add Skill Dialog */}
<Dialog
open={skillDialog}
onClose={() => setSkillDialog(false)}
onClose={(): void => setSkillDialog(false)}
maxWidth="sm"
fullWidth
fullScreen={isMobile}
@ -896,7 +902,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
fullWidth
label="Skill Name"
value={newSkill.name || ''}
onChange={e => setNewSkill({ ...newSkill, name: e.target.value })}
onChange={(e): void => setNewSkill({ ...newSkill, name: e.target.value })}
size={isMobile ? 'small' : 'medium'}
/>
</Grid>
@ -905,7 +911,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
fullWidth
label="Category"
value={newSkill.category || ''}
onChange={e => setNewSkill({ ...newSkill, category: e.target.value })}
onChange={(e): void => setNewSkill({ ...newSkill, category: e.target.value })}
placeholder="e.g., Programming, Design, Marketing"
size={isMobile ? 'small' : 'medium'}
/>
@ -915,7 +921,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
<InputLabel>Proficiency Level</InputLabel>
<Select
value={newSkill.level || 'beginner'}
onChange={e =>
onChange={(e): void =>
setNewSkill({
...newSkill,
level: e.target.value as Types.SkillLevel,
@ -936,7 +942,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
type="number"
label="Years of Experience"
value={newSkill.yearsOfExperience || 0}
onChange={e =>
onChange={(e): void =>
setNewSkill({
...newSkill,
yearsOfExperience: parseInt(e.target.value) || 0,
@ -955,7 +961,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
}}
>
<Button
onClick={() => setSkillDialog(false)}
onClick={(): void => setSkillDialog(false)}
fullWidth={isMobile}
size={isMobile ? 'small' : 'medium'}
>
@ -975,7 +981,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
{/* Add Experience Dialog */}
<Dialog
open={experienceDialog}
onClose={() => setExperienceDialog(false)}
onClose={(): void => setExperienceDialog(false)}
maxWidth="md"
fullWidth
fullScreen={isMobile}
@ -1003,7 +1009,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
fullWidth
label="Company Name"
value={newExperience.companyName || ''}
onChange={e =>
onChange={(e): void =>
setNewExperience({
...newExperience,
companyName: e.target.value,
@ -1017,7 +1023,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
fullWidth
label="Position/Title"
value={newExperience.position || ''}
onChange={e =>
onChange={(e): void =>
setNewExperience({
...newExperience,
position: e.target.value,
@ -1032,7 +1038,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
type="date"
label="Start Date"
value={newExperience.startDate?.toISOString().split('T')[0] || ''}
onChange={e =>
onChange={(e): void =>
setNewExperience({
...newExperience,
startDate: new Date(e.target.value),
@ -1047,7 +1053,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
control={
<Switch
checked={newExperience.isCurrent || false}
onChange={e =>
onChange={(e): void =>
setNewExperience({
...newExperience,
isCurrent: e.target.checked,
@ -1071,7 +1077,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
rows={isMobile ? 3 : 4}
label="Job Description"
value={newExperience.description || ''}
onChange={e =>
onChange={(e): void =>
setNewExperience({
...newExperience,
description: e.target.value,
@ -1091,7 +1097,7 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
}}
>
<Button
onClick={() => setExperienceDialog(false)}
onClick={(): void => setExperienceDialog(false)}
fullWidth={isMobile}
size={isMobile ? 'small' : 'medium'}
>
@ -1112,10 +1118,10 @@ const CandidateProfile: React.FC<BackstoryPageProps> = (_props: BackstoryPagePro
<Snackbar
open={snackbar.open}
autoHideDuration={6000}
onClose={() => setSnackbar({ ...snackbar, open: false })}
onClose={(): void => setSnackbar({ ...snackbar, open: false })}
>
<Alert
onClose={() => setSnackbar({ ...snackbar, open: false })}
onClose={(): void => setSnackbar({ ...snackbar, open: false })}
severity={snackbar.severity}
sx={{ width: '100%' }}
>

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { JSX, useState } from 'react';
import {
Paper,
Box,
@ -24,13 +24,13 @@ import {
InputAdornment,
} from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material';
import { ApiClient } from 'services/api-client';
import { ApiClient, RegistrationResponse } from 'services/api-client';
import { RegistrationSuccessDialog } from 'components/EmailVerificationComponents';
import { useAuth } from 'hooks/AuthContext';
import { useNavigate } from 'react-router-dom';
// Candidate Registration Form
const CandidateRegistrationForm = () => {
const CandidateRegistrationForm = (): JSX.Element => {
const { apiClient } = useAuth();
const navigate = useNavigate();
const [formData, setFormData] = useState({
@ -48,13 +48,13 @@ const CandidateRegistrationForm = () => {
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [showSuccess, setShowSuccess] = useState(false);
const [registrationResult, setRegistrationResult] = useState<any>(null);
const [registrationResult, setRegistrationResult] = useState<RegistrationResponse | null>(null);
// Password visibility states
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const validateForm = () => {
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
// Email validation
@ -128,7 +128,7 @@ const CandidateRegistrationForm = () => {
return errors.length > 0 ? [`Password must contain ${errors.join(', ')}`] : [];
};
const handleInputChange = (field: string, value: string) => {
const handleInputChange = (field: string, value: string): void => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear error when user starts typing
@ -137,7 +137,7 @@ const CandidateRegistrationForm = () => {
}
};
const handleSubmit = async () => {
const handleSubmit = async (): Promise<void> => {
if (!validateForm()) {
return;
}
@ -158,24 +158,30 @@ const CandidateRegistrationForm = () => {
setRegistrationResult(result);
setShowSuccess(true);
} catch (error: any) {
if (error.message.includes('already exists')) {
if (error.message.includes('email')) {
setErrors({ email: 'An account with this email already exists' });
} else if (error.message.includes('username')) {
setErrors({ username: 'This username is already taken' });
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('already exists')) {
if (error.message.includes('email')) {
setErrors({ email: 'An account with this email already exists' });
} else if (error.message.includes('username')) {
setErrors({ username: 'This username is already taken' });
}
} else {
setErrors({
general: error.message || 'Registration failed. Please try again.',
});
}
} else {
setErrors({
general: error.message || 'Registration failed. Please try again.',
});
console.error(error);
}
} finally {
setLoading(false);
}
};
const getPasswordStrength = (password: string) => {
const getPasswordStrength = (
password: string
): { level: string; color: string; value: number } => {
const validations = [
password.length >= 8,
/[a-z]/.test(password),
@ -210,7 +216,7 @@ const CandidateRegistrationForm = () => {
label="Email Address"
type="email"
value={formData.email}
onChange={e => handleInputChange('email', e.target.value)}
onChange={(e): void => handleInputChange('email', e.target.value)}
placeholder="your.email@example.com"
error={!!errors.email}
helperText={errors.email}
@ -221,7 +227,7 @@ const CandidateRegistrationForm = () => {
fullWidth
label="Username"
value={formData.username}
onChange={e => handleInputChange('username', e.target.value.toLowerCase())}
onChange={(e): void => handleInputChange('username', e.target.value.toLowerCase())}
placeholder="johndoe123"
error={!!errors.username}
helperText={errors.username}
@ -233,7 +239,7 @@ const CandidateRegistrationForm = () => {
fullWidth
label="First Name"
value={formData.firstName}
onChange={e => handleInputChange('firstName', e.target.value)}
onChange={(e): void => handleInputChange('firstName', e.target.value)}
placeholder="John"
error={!!errors.firstName}
helperText={errors.firstName}
@ -243,7 +249,7 @@ const CandidateRegistrationForm = () => {
fullWidth
label="Last Name"
value={formData.lastName}
onChange={e => handleInputChange('lastName', e.target.value)}
onChange={(e): void => handleInputChange('lastName', e.target.value)}
placeholder="Doe"
error={!!errors.lastName}
helperText={errors.lastName}
@ -256,7 +262,7 @@ const CandidateRegistrationForm = () => {
label="Phone Number"
type="tel"
value={formData.phone}
onChange={e => handleInputChange('phone', e.target.value)}
onChange={(e): void => handleInputChange('phone', e.target.value)}
placeholder="+1 (555) 123-4567"
error={!!errors.phone}
helperText={errors.phone || 'Optional'}
@ -268,7 +274,7 @@ const CandidateRegistrationForm = () => {
label="Password"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={e => handleInputChange('password', e.target.value)}
onChange={(e): void => handleInputChange('password', e.target.value)}
placeholder="Create a strong password"
error={!!errors.password}
helperText={errors.password}
@ -278,8 +284,8 @@ const CandidateRegistrationForm = () => {
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)}
onMouseDown={e => e.preventDefault()}
onClick={(): void => setShowPassword(!showPassword)}
onMouseDown={(e): void => e.preventDefault()}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
@ -293,6 +299,7 @@ const CandidateRegistrationForm = () => {
<LinearProgress
variant="determinate"
value={passwordStrength.value}
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
color={passwordStrength.color as any}
sx={{ height: 6, borderRadius: 3 }}
/>
@ -312,7 +319,7 @@ const CandidateRegistrationForm = () => {
label="Confirm Password"
type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword}
onChange={e => handleInputChange('confirmPassword', e.target.value)}
onChange={(e): void => handleInputChange('confirmPassword', e.target.value)}
placeholder="Confirm your password"
error={!!errors.confirmPassword}
helperText={errors.confirmPassword}
@ -322,8 +329,8 @@ const CandidateRegistrationForm = () => {
<InputAdornment position="end">
<IconButton
aria-label="toggle confirm password visibility"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
onMouseDown={e => e.preventDefault()}
onClick={(): void => setShowConfirmPassword(!showConfirmPassword)}
onMouseDown={(e): void => e.preventDefault()}
edge="end"
>
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
@ -358,7 +365,7 @@ const CandidateRegistrationForm = () => {
Already have an account?{' '}
<Link
component="button"
onClick={e => {
onClick={(e): void => {
e.preventDefault();
navigate('/login');
}}
@ -372,7 +379,7 @@ const CandidateRegistrationForm = () => {
{showSuccess && registrationResult && (
<RegistrationSuccessDialog
open={showSuccess}
onClose={() => setShowSuccess(false)}
onClose={(): void => setShowSuccess(false)}
email={registrationResult.email}
userType="candidate"
/>
@ -382,7 +389,7 @@ const CandidateRegistrationForm = () => {
};
// Employer Registration Form
const EmployerRegistrationForm = () => {
const EmployerRegistrationForm = (): JSX.Element => {
const [formData, setFormData] = useState({
email: '',
username: '',
@ -399,7 +406,7 @@ const EmployerRegistrationForm = () => {
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [showSuccess, setShowSuccess] = useState(false);
const [registrationResult, setRegistrationResult] = useState<any>(null);
const [registrationResult, setRegistrationResult] = useState<RegistrationResponse | null>(null);
// Password visibility states
const [showPassword, setShowPassword] = useState(false);
@ -430,7 +437,7 @@ const EmployerRegistrationForm = () => {
'1000+ employees',
];
const validateForm = () => {
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
// Email validation
@ -514,7 +521,7 @@ const EmployerRegistrationForm = () => {
return errors.length > 0 ? [`Password must contain ${errors.join(', ')}`] : [];
};
const handleInputChange = (field: string, value: string) => {
const handleInputChange = (field: string, value: string): void => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear error when user starts typing
@ -523,7 +530,7 @@ const EmployerRegistrationForm = () => {
}
};
const handleSubmit = async () => {
const handleSubmit = async (): Promise<void> => {
if (!validateForm()) {
return;
}
@ -547,17 +554,21 @@ const EmployerRegistrationForm = () => {
setRegistrationResult(result);
setShowSuccess(true);
} catch (error: any) {
if (error.message.includes('already exists')) {
if (error.message.includes('email')) {
setErrors({ email: 'An account with this email already exists' });
} else if (error.message.includes('username')) {
setErrors({ username: 'This username is already taken' });
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('already exists')) {
if (error.message.includes('email')) {
setErrors({ email: 'An account with this email already exists' });
} else if (error.message.includes('username')) {
setErrors({ username: 'This username is already taken' });
}
} else {
setErrors({
general: error.message || 'Registration failed. Please try again.',
});
}
} else {
setErrors({
general: error.message || 'Registration failed. Please try again.',
});
console.error(error);
}
} finally {
setLoading(false);
@ -590,7 +601,7 @@ const EmployerRegistrationForm = () => {
label="Email Address"
type="email"
value={formData.email}
onChange={e => handleInputChange('email', e.target.value)}
onChange={(e): void => handleInputChange('email', e.target.value)}
placeholder="company@example.com"
error={!!errors.email}
helperText={errors.email}
@ -600,7 +611,9 @@ const EmployerRegistrationForm = () => {
fullWidth
label="Username"
value={formData.username}
onChange={e => handleInputChange('username', e.target.value.toLowerCase())}
onChange={(e): void =>
handleInputChange('username', e.target.value.toLowerCase())
}
placeholder="company123"
error={!!errors.username}
helperText={errors.username}
@ -614,7 +627,7 @@ const EmployerRegistrationForm = () => {
label="Password"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={e => handleInputChange('password', e.target.value)}
onChange={(e): void => handleInputChange('password', e.target.value)}
placeholder="Create a strong password"
error={!!errors.password}
helperText={errors.password}
@ -624,8 +637,8 @@ const EmployerRegistrationForm = () => {
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={() => setShowPassword(!showPassword)}
onMouseDown={e => e.preventDefault()}
onClick={(): void => setShowPassword(!showPassword)}
onMouseDown={(e): void => e.preventDefault()}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
@ -639,7 +652,7 @@ const EmployerRegistrationForm = () => {
label="Confirm Password"
type={showConfirmPassword ? 'text' : 'password'}
value={formData.confirmPassword}
onChange={e => handleInputChange('confirmPassword', e.target.value)}
onChange={(e): void => handleInputChange('confirmPassword', e.target.value)}
placeholder="Confirm your password"
error={!!errors.confirmPassword}
helperText={errors.confirmPassword}
@ -649,8 +662,8 @@ const EmployerRegistrationForm = () => {
<InputAdornment position="end">
<IconButton
aria-label="toggle confirm password visibility"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
onMouseDown={e => e.preventDefault()}
onClick={(): void => setShowConfirmPassword(!showConfirmPassword)}
onMouseDown={(e): void => e.preventDefault()}
edge="end"
>
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
@ -674,7 +687,7 @@ const EmployerRegistrationForm = () => {
fullWidth
label="Company Name"
value={formData.companyName}
onChange={e => handleInputChange('companyName', e.target.value)}
onChange={(e): void => handleInputChange('companyName', e.target.value)}
placeholder="Your Company Inc."
error={!!errors.companyName}
helperText={errors.companyName}
@ -686,7 +699,7 @@ const EmployerRegistrationForm = () => {
<InputLabel>Industry</InputLabel>
<Select
value={formData.industry}
onChange={e => handleInputChange('industry', e.target.value)}
onChange={(e): void => handleInputChange('industry', e.target.value)}
label="Industry"
>
{industryOptions.map(industry => (
@ -702,7 +715,7 @@ const EmployerRegistrationForm = () => {
<InputLabel>Company Size</InputLabel>
<Select
value={formData.companySize}
onChange={e => handleInputChange('companySize', e.target.value)}
onChange={(e): void => handleInputChange('companySize', e.target.value)}
label="Company Size"
>
{companySizeOptions.map(size => (
@ -722,7 +735,7 @@ const EmployerRegistrationForm = () => {
multiline
rows={4}
value={formData.companyDescription}
onChange={e => handleInputChange('companyDescription', e.target.value)}
onChange={(e): void => handleInputChange('companyDescription', e.target.value)}
placeholder="Tell us about your company, what you do, your mission, and what makes you unique..."
error={!!errors.companyDescription}
helperText={
@ -739,7 +752,7 @@ const EmployerRegistrationForm = () => {
label="Website URL"
type="url"
value={formData.websiteUrl}
onChange={e => handleInputChange('websiteUrl', e.target.value)}
onChange={(e): void => handleInputChange('websiteUrl', e.target.value)}
placeholder="https://www.yourcompany.com"
error={!!errors.websiteUrl}
helperText={errors.websiteUrl || 'Optional'}
@ -749,7 +762,7 @@ const EmployerRegistrationForm = () => {
label="Phone Number"
type="tel"
value={formData.phone}
onChange={e => handleInputChange('phone', e.target.value)}
onChange={(e): void => handleInputChange('phone', e.target.value)}
placeholder="+1 (555) 123-4567"
error={!!errors.phone}
helperText={errors.phone || 'Optional'}
@ -792,7 +805,7 @@ const EmployerRegistrationForm = () => {
{showSuccess && registrationResult && (
<RegistrationSuccessDialog
open={showSuccess}
onClose={() => setShowSuccess(false)}
onClose={(): void => setShowSuccess(false)}
email={registrationResult.email}
userType="employer"
/>
@ -803,6 +816,7 @@ const EmployerRegistrationForm = () => {
// Registration Type Selector Component
export function RegistrationTypeSelector() {
const navigate = useNavigate();
return (
<Paper elevation={3}>
<Box sx={{ p: 5 }}>
@ -829,7 +843,9 @@ export function RegistrationTypeSelector() {
borderColor: 'primary.main',
},
}}
onClick={() => (window.location.href = '/register/candidate')}
onClick={(): void => {
navigate('/register/candidate');
}}
>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h1" sx={{ mb: 2 }}>
@ -862,7 +878,9 @@ export function RegistrationTypeSelector() {
borderColor: 'primary.main',
},
}}
onClick={() => (window.location.href = '/register/employer')}
onClick={(): void => {
navigate('/register/employer');
}}
>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="h1" sx={{ mb: 2 }}>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, ReactElement } from 'react';
import React, { useState, useEffect, ReactElement, JSX } from 'react';
// import FormGroup from '@mui/material/FormGroup';
// import FormControlLabel from '@mui/material/FormControlLabel';
// import Switch from '@mui/material/Switch';
@ -39,7 +39,7 @@ const SystemInfoComponent: React.FC<{
}> = ({ systemInfo }) => {
const [systemElements, setSystemElements] = useState<ReactElement[]>([]);
const convertToSymbols = (text: string) => {
const convertToSymbols = (text: string): string => {
return text
.replace(/\(R\)/g, '®') // Replace (R) with the ® symbol
.replace(/\(C\)/g, '©') // Replace (C) with the © symbol
@ -83,7 +83,7 @@ const SystemInfoComponent: React.FC<{
return <div className="SystemInfo">{systemElements}</div>;
};
const Settings = (_props: BackstoryPageProps) => {
const Settings = (_props: BackstoryPageProps): JSX.Element => {
const { apiClient } = useAuth();
const { setSnack } = useAppState();
// const [editSystemPrompt, setEditSystemPrompt] = useState<string>("");
@ -176,7 +176,7 @@ const Settings = (_props: BackstoryPageProps) => {
if (systemInfo !== undefined) {
return;
}
const fetchSystemInfo = async () => {
const fetchSystemInfo = async (): Promise<void> => {
try {
const response: Types.SystemInfo = await apiClient.getSystemInfo();
setSystemInfo(response);