Compare commits
2 Commits
7257e6d160
...
ff3e4605a1
Author | SHA1 | Date | |
---|---|---|---|
ff3e4605a1 | |||
2b1fbf2eaf |
@ -2,3 +2,5 @@ node_modules
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
src/types/*
|
||||
src/services/*
|
||||
|
165
frontend/package-lock.json
generated
165
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
}}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}}
|
||||
>
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -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' }}
|
||||
|
@ -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();
|
||||
}}
|
||||
|
@ -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);
|
||||
}}
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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">
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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(() => {
|
||||
|
@ -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');
|
||||
}}
|
||||
/>
|
||||
|
@ -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()) || ''
|
||||
);
|
||||
|
@ -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);
|
||||
}}
|
||||
>
|
||||
|
@ -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');
|
||||
};
|
||||
|
||||
@ -253,14 +253,17 @@ const HowItWorks: React.FC = () => {
|
||||
component="h1"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontSize: { xs: '1.5rem', md: '2rem' },
|
||||
fontSize: { xs: '1.25rem', md: '1.75rem' },
|
||||
mb: 2,
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
Your complete professional story, beyond a single page
|
||||
</Typography>
|
||||
<Typography variant="h5" sx={{ mb: 3, fontWeight: 400 }}>
|
||||
<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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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');
|
||||
}}
|
||||
|
@ -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%' }}
|
||||
>
|
||||
|
@ -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 }}>
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user