backstory/frontend/src/components/ui/ResumePreview.tsx

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 }}>&nbsp;</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;