533 lines
15 KiB
TypeScript
533 lines
15 KiB
TypeScript
import React from 'react';
|
|
import { Box, Typography, SxProps, Theme } from '@mui/material';
|
|
import {
|
|
Email as EmailIcon,
|
|
Phone as PhoneIcon,
|
|
LocationOn as LocationIcon,
|
|
} from '@mui/icons-material';
|
|
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
|
import { StyledMarkdown } from 'components/StyledMarkdown';
|
|
import * as Types from 'types/types';
|
|
|
|
import './ResumePreview.css';
|
|
|
|
// Resume Style Definitions
|
|
export 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 generateResumeStyles = (): Record<string, ResumeStyle> => {
|
|
return {
|
|
classic: {
|
|
name: 'Classic',
|
|
description: 'Traditional, professional serif design',
|
|
headerStyle: {
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
fontFamily: '"Times New Roman", Times, serif',
|
|
borderBottom: '2px solid #2c3e50',
|
|
paddingBottom: 2,
|
|
marginBottom: 3,
|
|
} as SxProps<Theme>,
|
|
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,
|
|
} as SxProps<Theme>,
|
|
contentStyle: {
|
|
fontFamily: '"Times New Roman", Times, serif',
|
|
lineHeight: 1.6,
|
|
color: '#2c3e50',
|
|
} as SxProps<Theme>,
|
|
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,
|
|
},
|
|
} as SxProps<Theme>,
|
|
color: {
|
|
primary: '#2c3e50',
|
|
secondary: '#34495e',
|
|
accent: '#3498db',
|
|
text: '#2c3e50',
|
|
background: '#ffffff',
|
|
},
|
|
},
|
|
modern: {
|
|
name: 'Modern',
|
|
description: 'Clean, minimalist sans-serif layout',
|
|
headerStyle: {
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
borderLeft: '4px solid #3498db',
|
|
paddingLeft: 2,
|
|
marginBottom: 3,
|
|
backgroundColor: '#f8f9fa',
|
|
padding: 2,
|
|
borderRadius: 1,
|
|
} as SxProps<Theme>,
|
|
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,
|
|
} as SxProps<Theme>,
|
|
contentStyle: {
|
|
fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
lineHeight: 1.5,
|
|
color: '#2c3e50',
|
|
} as SxProps<Theme>,
|
|
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,
|
|
},
|
|
} as SxProps<Theme>,
|
|
color: {
|
|
primary: '#3498db',
|
|
secondary: '#2c3e50',
|
|
accent: '#e74c3c',
|
|
text: '#2c3e50',
|
|
background: '#ffffff',
|
|
},
|
|
},
|
|
creative: {
|
|
name: 'Creative',
|
|
description: 'Colorful, unique design with personality',
|
|
headerStyle: {
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
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,
|
|
} as SxProps<Theme>,
|
|
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,
|
|
} as SxProps<Theme>,
|
|
contentStyle: {
|
|
fontFamily: '"Open Sans", Arial, sans-serif',
|
|
lineHeight: 1.6,
|
|
color: '#444444',
|
|
} as SxProps<Theme>,
|
|
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,
|
|
},
|
|
} as SxProps<Theme>,
|
|
color: {
|
|
primary: '#667eea',
|
|
secondary: '#764ba2',
|
|
accent: '#f093fb',
|
|
text: '#444444',
|
|
background: '#ffffff',
|
|
},
|
|
},
|
|
corporate: {
|
|
name: 'Corporate',
|
|
description: 'Formal, structured business format',
|
|
headerStyle: {
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
fontFamily: '"Arial", sans-serif',
|
|
border: '2px solid #34495e',
|
|
padding: 2.5,
|
|
marginBottom: 3,
|
|
backgroundColor: '#ecf0f1',
|
|
} as SxProps<Theme>,
|
|
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,
|
|
} as SxProps<Theme>,
|
|
contentStyle: {
|
|
fontFamily: '"Arial", sans-serif',
|
|
lineHeight: 1.4,
|
|
color: '#2c3e50',
|
|
} as SxProps<Theme>,
|
|
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,
|
|
},
|
|
} as SxProps<Theme>,
|
|
color: {
|
|
primary: '#34495e',
|
|
secondary: '#2c3e50',
|
|
accent: '#95a5a6',
|
|
text: '#2c3e50',
|
|
background: '#ffffff',
|
|
},
|
|
},
|
|
};
|
|
};
|
|
|
|
export const resumeStyles: Record<string, ResumeStyle> = generateResumeStyles();
|
|
|
|
// Styled Header Component
|
|
interface StyledHeaderProps {
|
|
candidate: Types.Candidate;
|
|
style: ResumeStyle;
|
|
}
|
|
|
|
const StyledHeader: React.FC<StyledHeaderProps> = ({ candidate, style }) => {
|
|
const phone = parsePhoneNumberFromString(candidate.phone || '', 'US');
|
|
|
|
return (
|
|
<Box className="BackstoryResumeHeader" sx={style.headerStyle}>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
|
<Box sx={{ display: 'flex' }}>
|
|
<Typography
|
|
variant="h4"
|
|
sx={{
|
|
fontWeight: 'bold',
|
|
mb: 1,
|
|
color: style.name === 'creative' ? '#ffffff' : style.color.primary,
|
|
fontFamily: 'inherit',
|
|
}}
|
|
>
|
|
{candidate.fullName}
|
|
</Typography>
|
|
</Box>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
gap: 1,
|
|
}}
|
|
>
|
|
{candidate.description && (
|
|
<Box sx={{ display: 'flex' }}>
|
|
<Typography
|
|
variant="h6"
|
|
sx={{
|
|
mb: 2,
|
|
fontWeight: 300,
|
|
color: style.name === 'creative' ? '#ffffff' : style.color.secondary,
|
|
fontFamily: 'inherit',
|
|
fontSize: '0.8rem !important',
|
|
}}
|
|
>
|
|
{candidate.description}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
flexWrap: 'wrap',
|
|
alignContent: 'center',
|
|
flexGrow: 1,
|
|
minWidth: 'fit-content',
|
|
gap: 1,
|
|
}}
|
|
>
|
|
{candidate.email && (
|
|
<Box sx={{ display: 'flex', alignItems: 'center', m: 0, p: 0 }}>
|
|
<EmailIcon
|
|
fontSize="small"
|
|
sx={{ mr: 1, 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>
|
|
)}
|
|
|
|
{phone?.isValid() && (
|
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
<PhoneIcon
|
|
fontSize="small"
|
|
sx={{ mr: 1, 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>
|
|
)}
|
|
|
|
{candidate.location && (
|
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
|
<LocationIcon
|
|
fontSize="small"
|
|
sx={{ mr: 1, 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.city}, ${candidate.location.state}`
|
|
: candidate.location.text}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
// Styled Footer Component
|
|
interface StyledFooterProps {
|
|
candidate: Types.Candidate;
|
|
job?: Types.Job;
|
|
style: ResumeStyle;
|
|
}
|
|
|
|
const StyledFooter: React.FC<StyledFooterProps> = ({ candidate, job, style }) => {
|
|
return (
|
|
<>
|
|
<Box
|
|
className="BackstoryResumeFooter"
|
|
sx={{
|
|
...style.footerStyle,
|
|
color: style.color.secondary,
|
|
}}
|
|
>
|
|
Dive deeper into my qualifications at Backstory...
|
|
<Box
|
|
component="img"
|
|
src={`/api/1.0/candidates/qr-code/${candidate.id || ''}/${(job && job.id) || ''}`}
|
|
alt="QR Code"
|
|
className="qr-code"
|
|
sx={{ display: 'flex', mt: 1, mb: 1 }}
|
|
/>
|
|
{candidate?.username
|
|
? `${window.location.protocol}://${window.location.host}/u/${candidate?.username}`
|
|
: 'backstory'}
|
|
</Box>
|
|
<Box sx={{ pb: 2 }}> </Box>
|
|
</>
|
|
);
|
|
};
|
|
|
|
// Main ResumePreview Component
|
|
export interface ResumePreviewProps {
|
|
resume: Types.Resume;
|
|
selectedStyle?: string;
|
|
shadeMargins?: boolean;
|
|
}
|
|
|
|
export const ResumePreview: React.FC<ResumePreviewProps> = (props: ResumePreviewProps) => {
|
|
const { resume, selectedStyle = 'corporate', shadeMargins = true } = props;
|
|
const currentStyle = resumeStyles[selectedStyle] || resumeStyles.corporate;
|
|
const job: Types.Job | null = resume.job || null;
|
|
const candidate: Types.Candidate | null = resume.candidate || null;
|
|
|
|
if (!resume || !candidate || !job) {
|
|
return (
|
|
<Box sx={{ p: 2 }}>
|
|
<Typography variant="body1">No resume data available.</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box
|
|
className="document-container"
|
|
sx={{
|
|
...currentStyle.contentStyle,
|
|
position: 'relative',
|
|
height: 'fit-content',
|
|
minHeight: 'fit-content',
|
|
display: 'flex',
|
|
m: 0,
|
|
p: 0,
|
|
}}
|
|
>
|
|
<Box
|
|
className={`a4-document ${shadeMargins ? 'with-margins' : ''}`}
|
|
sx={{
|
|
backgroundColor: currentStyle.color.background,
|
|
padding: 5,
|
|
minHeight: '100vh',
|
|
height: 'fit-content',
|
|
boxShadow: '0 0 10px rgba(0,0,0,0.1)',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
{/* Custom Header */}
|
|
<StyledHeader candidate={candidate} style={currentStyle} />
|
|
|
|
{/* Styled Markdown Content */}
|
|
<Box sx={currentStyle.markdownStyle}>
|
|
<StyledMarkdown
|
|
sx={{
|
|
position: 'relative',
|
|
maxHeight: '100%',
|
|
display: 'flex',
|
|
flexGrow: 1,
|
|
flex: 1,
|
|
...currentStyle.markdownStyle,
|
|
}}
|
|
content={resume.resume}
|
|
/>
|
|
</Box>
|
|
|
|
{/* QR Code Footer */}
|
|
{job && <StyledFooter candidate={candidate} job={job} style={currentStyle} />}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default ResumePreview;
|