Added resume styles
This commit is contained in:
parent
cacbb0fd0f
commit
d66e1ee1e4
1
frontend/package-lock.json
generated
1
frontend/package-lock.json
generated
@ -29,6 +29,7 @@
|
|||||||
"@uiw/react-markdown-editor": "^6.1.4",
|
"@uiw/react-markdown-editor": "^6.1.4",
|
||||||
"country-state-city": "^3.2.1",
|
"country-state-city": "^3.2.1",
|
||||||
"jsonrepair": "^3.12.0",
|
"jsonrepair": "^3.12.0",
|
||||||
|
"libphonenumber-js": "^1.12.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"luxon": "^3.6.1",
|
"luxon": "^3.6.1",
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"@uiw/react-markdown-editor": "^6.1.4",
|
"@uiw/react-markdown-editor": "^6.1.4",
|
||||||
"country-state-city": "^3.2.1",
|
"country-state-city": "^3.2.1",
|
||||||
"jsonrepair": "^3.12.0",
|
"jsonrepair": "^3.12.0",
|
||||||
|
"libphonenumber-js": "^1.12.9",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.511.0",
|
"lucide-react": "^0.511.0",
|
||||||
"luxon": "^3.6.1",
|
"luxon": "^3.6.1",
|
||||||
|
@ -14,7 +14,7 @@ interface CandidatePickerProps extends BackstoryElementProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CandidatePicker = (props: CandidatePickerProps): JSX.Element => {
|
const CandidatePicker = (props: CandidatePickerProps): JSX.Element => {
|
||||||
const { onSelect, sx } = props;
|
const { onSelect } = props;
|
||||||
const { apiClient } = useAuth();
|
const { apiClient } = useAuth();
|
||||||
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
const { selectedCandidate, setSelectedCandidate } = useSelectedCandidate();
|
||||||
const { setSnack } = useAppState();
|
const { setSnack } = useAppState();
|
||||||
|
@ -73,7 +73,7 @@ interface JobsViewProps {
|
|||||||
onJobSelect?: (selectedJobs: Types.Job[]) => void;
|
onJobSelect?: (selectedJobs: Types.Job[]) => void;
|
||||||
onJobView?: (job: Types.Job) => void;
|
onJobView?: (job: Types.Job) => void;
|
||||||
onJobEdit?: (job: Types.Job) => void;
|
onJobEdit?: (job: Types.Job) => void;
|
||||||
onJobDelete?: (job: Types.Job) => void;
|
onJobDelete?: (job: Types.Job) => Promise<void>;
|
||||||
selectable?: boolean;
|
selectable?: boolean;
|
||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
showDetailsPanel?: boolean;
|
showDetailsPanel?: boolean;
|
||||||
@ -609,7 +609,13 @@ const JobsView: React.FC<JobsViewProps> = ({
|
|||||||
)}
|
)}
|
||||||
{onJobDelete && (
|
{onJobDelete && (
|
||||||
<Tooltip title="Delete Job">
|
<Tooltip title="Delete Job">
|
||||||
<IconButton size="small" onClick={(): void => onJobDelete(job)}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={async (): Promise<void> => {
|
||||||
|
await onJobDelete(job);
|
||||||
|
fetchJobs(0, searchQuery);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<DeleteIcon fontSize="small" />
|
<DeleteIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -136,4 +136,17 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.BackstoryResumeHeader {
|
||||||
|
gap: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* border: 3px solid orange; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.BackstoryResumeHeader p {
|
||||||
|
/* border: 3px solid purple; */
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,12 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
Paper,
|
Paper,
|
||||||
|
FormControl,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
InputLabel,
|
||||||
|
Chip,
|
||||||
|
Theme,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import PrintIcon from '@mui/icons-material/Print';
|
import PrintIcon from '@mui/icons-material/Print';
|
||||||
import {
|
import {
|
||||||
@ -34,11 +40,17 @@ import {
|
|||||||
Person as PersonIcon,
|
Person as PersonIcon,
|
||||||
Schedule as ScheduleIcon,
|
Schedule as ScheduleIcon,
|
||||||
ModelTraining,
|
ModelTraining,
|
||||||
|
Style as StyleIcon,
|
||||||
|
Email as EmailIcon,
|
||||||
|
Phone as PhoneIcon,
|
||||||
|
LocationOn as LocationIcon,
|
||||||
|
// Language as WebsiteIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import InputIcon from '@mui/icons-material/Input';
|
import InputIcon from '@mui/icons-material/Input';
|
||||||
import TuneIcon from '@mui/icons-material/Tune';
|
import TuneIcon from '@mui/icons-material/Tune';
|
||||||
import PreviewIcon from '@mui/icons-material/Preview';
|
import PreviewIcon from '@mui/icons-material/Preview';
|
||||||
import EditDocumentIcon from '@mui/icons-material/EditDocument';
|
import EditDocumentIcon from '@mui/icons-material/EditDocument';
|
||||||
|
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
||||||
|
|
||||||
import { useReactToPrint } from 'react-to-print';
|
import { useReactToPrint } from 'react-to-print';
|
||||||
|
|
||||||
@ -62,6 +74,450 @@ interface ResumeInfoProps {
|
|||||||
variant?: 'minimal' | 'small' | 'normal' | 'all' | null;
|
variant?: 'minimal' | 'small' | 'normal' | 'all' | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resume Style Definitions
|
||||||
|
interface ResumeStyle {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
headerStyle: SxProps<Theme>;
|
||||||
|
footerStyle: SxProps<Theme>;
|
||||||
|
contentStyle: SxProps<Theme>;
|
||||||
|
markdownStyle: SxProps<Theme>;
|
||||||
|
color: {
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
accent: string;
|
||||||
|
text: string;
|
||||||
|
background: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resumeStyles: Record<string, ResumeStyle> = {
|
||||||
|
classic: {
|
||||||
|
name: 'Classic',
|
||||||
|
description: 'Traditional, professional serif design',
|
||||||
|
headerStyle: {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
borderBottom: '2px solid #2c3e50',
|
||||||
|
paddingBottom: 2,
|
||||||
|
marginBottom: 3,
|
||||||
|
},
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
borderTop: '2px solid #2c3e50',
|
||||||
|
paddingTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
color: '#2c3e50',
|
||||||
|
},
|
||||||
|
markdownStyle: {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Times New Roman", Times, serif',
|
||||||
|
color: '#2c3e50',
|
||||||
|
borderBottom: '1px solid #bdc3c7',
|
||||||
|
paddingBottom: 1,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.6,
|
||||||
|
marginBottom: 1,
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
primary: '#2c3e50',
|
||||||
|
secondary: '#34495e',
|
||||||
|
accent: '#3498db',
|
||||||
|
text: '#2c3e50',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modern: {
|
||||||
|
name: 'Modern',
|
||||||
|
description: 'Clean, minimalist sans-serif layout',
|
||||||
|
headerStyle: {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
borderLeft: '4px solid #3498db',
|
||||||
|
paddingLeft: 2,
|
||||||
|
marginBottom: 3,
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
},
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
borderLeft: '4px solid #3498db',
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
paddingTop: 2,
|
||||||
|
borderRadius: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
lineHeight: 1.5,
|
||||||
|
color: '#2c3e50',
|
||||||
|
},
|
||||||
|
markdownStyle: {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
|
color: '#3498db',
|
||||||
|
fontWeight: 300,
|
||||||
|
marginBottom: 1.5,
|
||||||
|
},
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1.75rem',
|
||||||
|
},
|
||||||
|
'& h2': {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.5,
|
||||||
|
marginBottom: 0.75,
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 2.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
primary: '#3498db',
|
||||||
|
secondary: '#2c3e50',
|
||||||
|
accent: '#e74c3c',
|
||||||
|
text: '#2c3e50',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
creative: {
|
||||||
|
name: 'Creative',
|
||||||
|
description: 'Colorful, unique design with personality',
|
||||||
|
headerStyle: {
|
||||||
|
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
color: '#ffffff',
|
||||||
|
padding: 2.5,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
marginBottom: 3,
|
||||||
|
},
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
color: '#ffffff',
|
||||||
|
paddingTop: 2,
|
||||||
|
borderRadius: 1.5,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
fontFamily: '"Open Sans", Arial, sans-serif',
|
||||||
|
lineHeight: 1.6,
|
||||||
|
color: '#444444',
|
||||||
|
},
|
||||||
|
markdownStyle: {
|
||||||
|
fontFamily: '"Open Sans", Arial, sans-serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Montserrat", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
color: '#667eea',
|
||||||
|
fontWeight: 600,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
},
|
||||||
|
'& h2': {
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.6,
|
||||||
|
marginBottom: 1,
|
||||||
|
color: '#444444',
|
||||||
|
},
|
||||||
|
'& strong': {
|
||||||
|
color: '#764ba2',
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
primary: '#667eea',
|
||||||
|
secondary: '#764ba2',
|
||||||
|
accent: '#f093fb',
|
||||||
|
text: '#444444',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
corporate: {
|
||||||
|
name: 'Corporate',
|
||||||
|
description: 'Formal, structured business format',
|
||||||
|
headerStyle: {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
border: '2px solid #34495e',
|
||||||
|
padding: 2.5,
|
||||||
|
marginBottom: 3,
|
||||||
|
backgroundColor: '#ecf0f1',
|
||||||
|
},
|
||||||
|
footerStyle: {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
border: '2px solid #34495e',
|
||||||
|
backgroundColor: '#ecf0f1',
|
||||||
|
paddingTop: 2,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
},
|
||||||
|
contentStyle: {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
lineHeight: 1.4,
|
||||||
|
color: '#2c3e50',
|
||||||
|
},
|
||||||
|
markdownStyle: {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
'& h1, & h2, & h3': {
|
||||||
|
fontFamily: '"Arial", sans-serif',
|
||||||
|
color: '#34495e',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
letterSpacing: '1px',
|
||||||
|
marginBottom: 1.5,
|
||||||
|
borderBottom: '1px solid #bdc3c7',
|
||||||
|
paddingBottom: 0.5,
|
||||||
|
},
|
||||||
|
'& h1': {
|
||||||
|
fontSize: '1rem',
|
||||||
|
},
|
||||||
|
'& h2': {
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
'& p, & li': {
|
||||||
|
lineHeight: 1.4,
|
||||||
|
marginBottom: 0.75,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
'& ul': {
|
||||||
|
paddingLeft: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
primary: '#34495e',
|
||||||
|
secondary: '#2c3e50',
|
||||||
|
accent: '#95a5a6',
|
||||||
|
text: '#2c3e50',
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Styled Header Component
|
||||||
|
interface BackstoryStyledResumeProps {
|
||||||
|
candidate: Types.Candidate;
|
||||||
|
style: ResumeStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledFooter: React.FC<BackstoryStyledResumeProps> = ({ candidate, style }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
className="BackstoryResumeFooter"
|
||||||
|
sx={{
|
||||||
|
...style.footerStyle,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
alignContent: 'center',
|
||||||
|
fontSize: '0.8rem',
|
||||||
|
pb: 2,
|
||||||
|
mb: 2,
|
||||||
|
// pt: 2,
|
||||||
|
color: style.color.secondary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Ask any questions you may have at my Backstory...
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
src={`/api/1.0/candidates/qr-code/${candidate.id || ''}`}
|
||||||
|
alt="QR Code"
|
||||||
|
className="qr-code"
|
||||||
|
sx={{ display: 'flex', mt: 1, mb: 1 }}
|
||||||
|
/>
|
||||||
|
{candidate?.username
|
||||||
|
? `https://backstory.ketrenos.com/u/${candidate?.username}`
|
||||||
|
: 'backstory'}
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ pb: 2 }}> </Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledHeader: React.FC<BackstoryStyledResumeProps> = ({ candidate, style }) => {
|
||||||
|
const phone = parsePhoneNumberFromString(candidate.phone || '', 'US');
|
||||||
|
return (
|
||||||
|
<Box className="BackstoryResumeHeader" sx={style.headerStyle}>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
mb: 1,
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.primary,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.fullName}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* {candidate.title && (
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
mb: 2,
|
||||||
|
fontWeight: 300,
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.secondary,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.title}
|
||||||
|
</Typography>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.email && (
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<EmailIcon
|
||||||
|
fontSize="small"
|
||||||
|
sx={{ color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.email}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phone?.isValid() && (
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<PhoneIcon
|
||||||
|
fontSize="small"
|
||||||
|
sx={{ color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{phone.formatInternational()}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{candidate.location && (
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<LocationIcon
|
||||||
|
fontSize="small"
|
||||||
|
sx={{ color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.location.city}, {candidate.location.state}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* {(candidate.website || candidate.linkedin) && (
|
||||||
|
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
|
<WebsiteIcon
|
||||||
|
fontSize="small"
|
||||||
|
sx={{ color: style.name === 'creative' ? '#ffffff' : style.color.accent }}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: style.name === 'creative' ? '#ffffff' : style.color.text,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{candidate.website || candidate.linkedin}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
)} */}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
||||||
const { setSnack } = useAppState();
|
const { setSnack } = useAppState();
|
||||||
const { resume } = props;
|
const { resume } = props;
|
||||||
@ -81,6 +537,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
const [status, setStatus] = useState<string>('');
|
const [status, setStatus] = useState<string>('');
|
||||||
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
|
const [statusType, setStatusType] = useState<Types.ApiActivityType | null>(null);
|
||||||
const [error, setError] = useState<Types.ChatMessageError | null>(null);
|
const [error, setError] = useState<Types.ChatMessageError | null>(null);
|
||||||
|
const [selectedStyle, setSelectedStyle] = useState<string>('modern');
|
||||||
|
|
||||||
const printContentRef = useRef<HTMLDivElement>(null);
|
const printContentRef = useRef<HTMLDivElement>(null);
|
||||||
const reactToPrintFn = useReactToPrint({
|
const reactToPrintFn = useReactToPrint({
|
||||||
@ -94,7 +551,9 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
}
|
}
|
||||||
}, [resume, activeResume]);
|
}, [resume, activeResume]);
|
||||||
|
|
||||||
// Check if content needs truncation
|
const currentStyle = resumeStyles[selectedStyle];
|
||||||
|
|
||||||
|
// Rest of the component remains the same...
|
||||||
const deleteResume = async (id: string | undefined): Promise<void> => {
|
const deleteResume = async (id: string | undefined): Promise<void> => {
|
||||||
if (id) {
|
if (id) {
|
||||||
try {
|
try {
|
||||||
@ -195,7 +654,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
const generateResume = async (): Promise<void> => {
|
const generateResume = async (): Promise<void> => {
|
||||||
setStatusType('thinking');
|
setStatusType('thinking');
|
||||||
setStatus('Starting resume generation...');
|
setStatus('Starting resume generation...');
|
||||||
setActiveResume({ ...activeResume, resume: '' }); // Reset resume content
|
setActiveResume({ ...activeResume, resume: '' });
|
||||||
const request = await apiClient.generateResume(
|
const request = await apiClient.generateResume(
|
||||||
activeResume.candidateId || '',
|
activeResume.candidateId || '',
|
||||||
activeResume.jobId || '',
|
activeResume.jobId || '',
|
||||||
@ -210,7 +669,6 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newValue === 'regenerate') {
|
if (newValue === 'regenerate') {
|
||||||
// Handle resume regeneration logic here
|
|
||||||
setSnack('Regenerating resume...');
|
setSnack('Regenerating resume...');
|
||||||
generateResume();
|
generateResume();
|
||||||
return;
|
return;
|
||||||
@ -487,18 +945,65 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tabs value={tabValue} onChange={handleTabChange} centered>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||||
<Tab value="markdown" icon={<EditDocumentIcon />} label="Markdown" />
|
<Tabs value={tabValue} onChange={handleTabChange}>
|
||||||
{activeResume.systemPrompt && (
|
<Tab value="markdown" icon={<EditDocumentIcon />} label="Markdown" />
|
||||||
<Tab value="systemPrompt" icon={<TuneIcon />} label="System Prompt" />
|
{activeResume.systemPrompt && (
|
||||||
)}
|
<Tab value="systemPrompt" icon={<TuneIcon />} label="System Prompt" />
|
||||||
{activeResume.systemPrompt && (
|
)}
|
||||||
<Tab value="prompt" icon={<InputIcon />} label="Prompt" />
|
{activeResume.systemPrompt && (
|
||||||
)}
|
<Tab value="prompt" icon={<InputIcon />} label="Prompt" />
|
||||||
<Tab value="preview" icon={<PreviewIcon />} label="Preview" />
|
)}
|
||||||
<Tab value="print" icon={<PrintIcon />} label="Print" />
|
<Tab value="preview" icon={<PreviewIcon />} label="Preview" />
|
||||||
<Tab value="regenerate" icon={<ModelTraining />} label="Regenerate" />
|
<Tab value="print" icon={<PrintIcon />} label="Print" />
|
||||||
</Tabs>
|
<Tab value="regenerate" icon={<ModelTraining />} label="Regenerate" />
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Style Selector */}
|
||||||
|
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||||||
|
<InputLabel id="resume-style-label">
|
||||||
|
<StyleIcon sx={{ mr: 1, fontSize: 16 }} />
|
||||||
|
Resume Style
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="resume-style-label"
|
||||||
|
value={selectedStyle}
|
||||||
|
onChange={e => setSelectedStyle(e.target.value)}
|
||||||
|
label="Resume Style"
|
||||||
|
>
|
||||||
|
{Object.entries(resumeStyles).map(([key, style]) => (
|
||||||
|
<MenuItem key={key} value={key}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="body2" fontWeight="bold">
|
||||||
|
{style.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{style.description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{/* Style Preview Chip */}
|
||||||
|
<Chip
|
||||||
|
icon={<StyleIcon />}
|
||||||
|
label={currentStyle.name}
|
||||||
|
sx={{
|
||||||
|
backgroundColor: currentStyle.color.primary,
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{status && (
|
{status && (
|
||||||
<Box sx={{ mt: 0, mb: 1 }}>
|
<Box sx={{ mt: 0, mb: 1 }}>
|
||||||
<StatusBox>
|
<StatusBox>
|
||||||
@ -514,12 +1019,11 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
height: '100%' /* Restrict to main-container's height */,
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
minHeight: 0 /* Prevent flex overflow */,
|
minHeight: 0,
|
||||||
//maxHeight: "min-content",
|
|
||||||
'& > *:not(.Scrollable)': {
|
'& > *:not(.Scrollable)': {
|
||||||
flexShrink: 0 /* Prevent shrinking */,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
}}
|
}}
|
||||||
@ -535,10 +1039,9 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
|
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flex: 1 /* Take remaining space in some-container */,
|
flex: 1,
|
||||||
overflowY: 'auto' /* Scroll if content overflows */,
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
placeholder="Enter resume content..."
|
placeholder="Enter resume content..."
|
||||||
/>
|
/>
|
||||||
@ -550,13 +1053,12 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
// height: '100%',
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flex: 1 /* Take remaining space in some-container */,
|
flex: 1,
|
||||||
overflowY: 'auto' /* Scroll if content overflows */,
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
placeholder="Edit system prompt..."
|
placeholder="Edit system prompt..."
|
||||||
/>
|
/>
|
||||||
@ -572,52 +1074,53 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
|
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
flex: 1 /* Take remaining space in some-container */,
|
flex: 1,
|
||||||
overflowY: 'auto' /* Scroll if content overflows */,
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
placeholder="Edit prompt..."
|
placeholder="Edit prompt..."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tabValue === 'preview' && (
|
{tabValue === 'preview' && (
|
||||||
<Box className="document-container" ref={printContentRef}>
|
<Box
|
||||||
<Box className="a4-document">
|
className="document-container"
|
||||||
<StyledMarkdown
|
ref={printContentRef}
|
||||||
sx={{
|
sx={currentStyle.contentStyle}
|
||||||
position: 'relative',
|
>
|
||||||
maxHeight: '100%',
|
<Box
|
||||||
display: 'flex',
|
className="a4-document"
|
||||||
flexGrow: 1,
|
sx={{
|
||||||
flex: 1 /* Take remaining space in some-container */,
|
backgroundColor: currentStyle.color.background,
|
||||||
// overflowY: 'auto' /* Scroll if content overflows */,
|
padding: 5,
|
||||||
}}
|
minHeight: '100vh',
|
||||||
content={editContent}
|
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
||||||
/>
|
}}
|
||||||
<Box
|
>
|
||||||
sx={{
|
{/* Custom Header */}
|
||||||
display: 'flex',
|
{activeResume.candidate && (
|
||||||
flexDirection: 'column',
|
<StyledHeader candidate={activeResume.candidate} style={currentStyle} />
|
||||||
alignItems: 'center',
|
)}
|
||||||
justifyContent: 'center',
|
|
||||||
textTransform: 'uppercase',
|
{/* Styled Markdown Content */}
|
||||||
fontSize: '0.8rem',
|
<Box sx={currentStyle.markdownStyle}>
|
||||||
pb: 2,
|
<StyledMarkdown
|
||||||
}}
|
sx={{
|
||||||
>
|
position: 'relative',
|
||||||
See my full Backstory at...
|
maxHeight: '100%',
|
||||||
<Box
|
display: 'flex',
|
||||||
component="img"
|
flexGrow: 1,
|
||||||
src={`/api/1.0/candidates/qr-code/${activeResume.candidateId || ''}`}
|
flex: 1,
|
||||||
alt="QR Code"
|
...currentStyle.markdownStyle,
|
||||||
className="qr-code"
|
}}
|
||||||
|
content={editContent || activeResume.resume || ''}
|
||||||
/>
|
/>
|
||||||
{activeResume.candidate?.username
|
|
||||||
? `https://backstory.ketrenos.com/u/${activeResume.candidate?.username}`
|
|
||||||
: 'backstory'}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{/* QR Code Footer */}
|
||||||
|
{activeResume.candidate && (
|
||||||
|
<StyledFooter candidate={activeResume.candidate} style={currentStyle} />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ pb: 2 }}> </Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Scrollable>
|
</Scrollable>
|
||||||
@ -647,7 +1150,7 @@ const ResumeInfo: React.FC<ResumeInfoProps> = (props: ResumeInfoProps) => {
|
|||||||
job={activeResume.job}
|
job={activeResume.job}
|
||||||
sx={{
|
sx={{
|
||||||
mt: 2,
|
mt: 2,
|
||||||
backgroundColor: '#f8f0e0', //theme.palette.background.paper,
|
backgroundColor: '#f8f0e0',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -27,11 +27,9 @@ import { VectorVisualizer } from 'components/VectorVisualizer';
|
|||||||
import { DocumentManager } from 'components/DocumentManager';
|
import { DocumentManager } from 'components/DocumentManager';
|
||||||
import { useAuth } from 'hooks/AuthContext';
|
import { useAuth } from 'hooks/AuthContext';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { JobsView } from 'components/ui/JobsView';
|
import { JobsViewPage } from 'pages/JobsViewPage';
|
||||||
import { ResumeViewer } from 'components/ui/ResumeViewer';
|
import { ResumeViewer } from 'components/ui/ResumeViewer';
|
||||||
|
|
||||||
import * as Types from 'types/types';
|
|
||||||
|
|
||||||
const LogoutPage = (): JSX.Element => {
|
const LogoutPage = (): JSX.Element => {
|
||||||
const { logout } = useAuth();
|
const { logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -127,16 +125,7 @@ export const navigationConfig: NavigationConfig = {
|
|||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
path: '/candidate/jobs/:jobId?',
|
path: '/candidate/jobs/:jobId?',
|
||||||
icon: <WorkIcon />,
|
icon: <WorkIcon />,
|
||||||
component: (
|
component: <JobsViewPage />,
|
||||||
<JobsView
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
variant: 'fullWidth',
|
variant: 'fullWidth',
|
||||||
userTypes: ['candidate', 'guest', 'employer'],
|
userTypes: ['candidate', 'guest', 'employer'],
|
||||||
showInNavigation: false,
|
showInNavigation: false,
|
||||||
|
@ -29,7 +29,6 @@ import waitPng from 'assets/wait.png';
|
|||||||
import finalResumePng from 'assets/final-resume.png';
|
import finalResumePng from 'assets/final-resume.png';
|
||||||
|
|
||||||
import { Beta } from 'components/ui/Beta';
|
import { Beta } from 'components/ui/Beta';
|
||||||
import { Quote } from '@uiw/react-json-view';
|
|
||||||
|
|
||||||
// Styled components matching HomePage patterns
|
// Styled components matching HomePage patterns
|
||||||
const HeroSection = styled(Box)(({ theme }) => ({
|
const HeroSection = styled(Box)(({ theme }) => ({
|
||||||
|
@ -77,6 +77,8 @@ class GenerateResume(Agent):
|
|||||||
Create a polished, concise, and ATS-friendly resume for the candidate based on the assessment data provided. Rephrase skills to avoid
|
Create a polished, concise, and ATS-friendly resume for the candidate based on the assessment data provided. Rephrase skills to avoid
|
||||||
direct duplication from the assessment.
|
direct duplication from the assessment.
|
||||||
|
|
||||||
|
Do not provide header information like name, email, or phone number in the resume, as that information will be added later.
|
||||||
|
|
||||||
## CANDIDATE INFORMATION:
|
## CANDIDATE INFORMATION:
|
||||||
Name: {self.user.full_name}
|
Name: {self.user.full_name}
|
||||||
Email: {self.user.email or "N/A"}
|
Email: {self.user.email or "N/A"}
|
||||||
@ -164,7 +166,7 @@ ELSE:
|
|||||||
Provide template format only
|
Provide template format only
|
||||||
|
|
||||||
## OUTPUT FORMAT:
|
## OUTPUT FORMAT:
|
||||||
Provide the resume in clean markdown format, ready for the candidate to use.
|
Provide the resume in clean markdown format, ready for the candidate to use. Do not provide header contact information, as that will be added later.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
prompt = """\
|
prompt = """\
|
||||||
|
Loading…
x
Reference in New Issue
Block a user