jsonresume-theme-stackoverflowed
Version:
A JSON Resume Theme inspired by the Stack Overflow Developer Story resume format.
1,145 lines (1,096 loc) • 41.3 kB
JavaScript
import { jsx, jsxs, Fragment } from '@emotion/react/jsx-runtime';
import { renderToString } from 'react-dom/server';
import { createContext, useContext, useMemo, Fragment as Fragment$1 } from 'react';
import countries from 'i18n-iso-countries';
import { parseISO, isValid, formatISO, format, fromUnixTime, formatISODuration, intervalToDuration } from 'date-fns';
import crypto from 'crypto';
import * as icons from 'simple-icons/icons';
import { ThemeProvider, Global } from '@emotion/react';
import { compose } from 'immer-compose';
import axios from 'axios';
const THEME_NAME = 'theme-stackoverflowed';
// IMPORTANT: must exist in `date-fns/locale`
const DEFAULT_LOCALE = 'en-US';
const TAG_STACKOVERFLOW = 'stackoverflow';
const shortCode = (locale) => (locale ?? '').split(/_|-/)[0].toLowerCase();
const importLocale$2 = (locale) => {
{
return import(
/* @vite-ignore */ `i18n-iso-countries/langs/${locale}.json`);
}
};
const countryNameFactory = async (locale) => {
let data;
let shortLocale;
try {
shortLocale = shortCode(locale);
data = await importLocale$2(shortLocale);
}
catch (e) {
shortLocale = shortCode(DEFAULT_LOCALE);
data = await importLocale$2(shortCode(shortLocale));
}
countries.registerLocale(data);
return {
countryName: (code) => {
if (!code) {
return null;
}
return {
countryNameOfficial: countries.getName(code, shortLocale, {
select: 'official',
}),
countryNameAlias: countries.getName(code, shortLocale, {
select: 'alias',
}),
};
},
};
};
const DEFAULT_DATE_FORMAT = 'MMM yyyy';
const importLocale$1 = (locale) => {
{
return import(/* @vite-ignore */ `date-fns/locale/${locale}/index.js`);
}
};
const getLocaleData = async (locale) => {
let localeData;
try {
localeData = await importLocale$1(locale);
}
catch (e) {
try {
if (shortCode(locale) === locale) {
throw 'no point... try next';
}
localeData = await importLocale$1(shortCode(locale));
}
catch (e) {
localeData = await importLocale$1(DEFAULT_LOCALE);
}
}
return localeData;
};
const dateFormatterFactory = async (locale) => {
const localeData = await getLocaleData(locale);
const formatDate = (date, format$1) => {
const _date = parseISO(date);
const _format = format$1 ? format$1 : DEFAULT_DATE_FORMAT;
if (!isValid(_date)) {
return null;
}
return {
dateISO: formatISO(_date),
date: format(_date, _format, { locale: localeData }),
};
};
const formatDateRange = ({ startDate, endDate, format: format$1, }) => {
const start = parseISO(startDate);
const _format = format$1 ? format$1 : DEFAULT_DATE_FORMAT;
let end;
if (!isValid(start)) {
return null;
}
if (endDate) {
end = parseISO(endDate);
if (!isValid(end)) {
return null;
}
}
else {
end = fromUnixTime(Date.now());
}
return {
durationISO: formatISODuration(intervalToDuration({ start, end })),
startDateISO: formatISO(start),
endDateISO: formatISO(end),
startDate: format(start, _format, { locale: localeData }),
endDate: format(end, _format, { locale: localeData }),
};
};
return {
formatDate,
formatDateRange,
};
};
let md5_;
{
md5_ = (str) => crypto.createHash('md5').update(str).digest('hex');
}
const md5 = md5_;
const asArray = (x) => (Array.isArray(x) ? x : [x]).slice();
const filterAny = (...predicates) => (x) => predicates.some((predicate) => predicate(x));
const filterPopulated = (...keys) => (obj) => obj &&
keys.every((key) => obj[key] !== undefined && obj[key] !== null && obj[key] !== '');
const RE_INTERPOLATE = /\{\{([a-z0-9_.-]+)\}\}/gi;
const interpolate = (template, values) => {
if (template === undefined) {
return '';
}
let err = false;
const interpolated = template.replace(RE_INTERPOLATE, (_, $1) => {
if (!($1 in values)) {
err = true;
return '';
}
if (values[$1] === undefined || values[$1] === null) {
err = true;
return '';
}
return String(values[$1]);
});
return err ? null : interpolated;
};
const normaliseTag = (name) => String(name).toLowerCase().replace(/\s+/g, '');
const capitalise = (word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
const getIconSvg = (key) => icons[`si${capitalise(key)}`].svg;
const importLocale = (locale) => {
{
return import(`../i18n/locales/${locale}.json`);
}
};
const localCopyFactory = async (locale) => {
let data;
try {
data = await importLocale(locale);
}
catch (e) {
try {
if (shortCode(locale) === locale) {
throw 'no point... try next';
}
data = await importLocale(shortCode(locale));
}
catch (e) {
data = await importLocale(shortCode(DEFAULT_LOCALE));
}
}
return {
i18n: (key, values = {}) => {
return interpolate(data.language[key], values) ?? '';
},
};
};
const MEMO = {};
const getLocale = async (locale = DEFAULT_LOCALE) => {
if (!(locale in MEMO)) {
const [countryNameFormatters, dateFormatters, copyFormatters] = await Promise.all([
countryNameFactory(locale),
dateFormatterFactory(locale),
localCopyFactory(locale),
]);
MEMO[locale] = {
...countryNameFormatters,
...dateFormatters,
...copyFormatters,
};
}
return MEMO[locale];
};
const AppContext = createContext(null);
const useAppContext = () => useContext(AppContext);
const useResume = () => useAppContext()?.resume;
const useLocale = (key) => useAppContext()?.locale?.[key];
const useConfig = (key) => useAppContext()?.resume?.meta?.[THEME_NAME]?.[key];
// Tagging literals as "html" lets prettier format the template.
const html = (...a) => String.raw(...a).trim();
const template = ({ body, meta, resume }) => html `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<meta name="generator" content="${meta.generator}" />
<title>
${[resume?.basics?.name, resume?.basics?.label, 'Resume']
.filter(Boolean)
.join(' - ')}
</title>
</head>
<body>
<div id="root">${body}</div>
</body>
</html>
`;
const palette = {
black: '#333333',
grey: '#666666',
lightGrey: '#adadad',
darkBlue: '#245895',
lightBlue: '#7aa8d7',
lighterBlue: '#a6c2dd',
};
const theme = {
text: {
font: {
primary: 'Arial, Helvetica, sans-serif',
secondary: 'Georgia, "Times New Roman", serif',
},
color: {
title: palette.darkBlue,
subTitle: palette.lightBlue,
primary: palette.black,
secondary: palette.grey,
tertiary: palette.lightGrey,
link: palette.black,
linkHover: palette.darkBlue,
},
},
spacing: {
color: {
divider: palette.lighterBlue,
},
},
when: (context, css) => {
const [media, width] = Array.isArray(context)
? context
: [context, undefined];
return {
...(media === 'print' &&
!width && {
'@media only print': css,
}),
...(media === 'screen' &&
!width && {
'@media only screen': css,
}),
...(media === 'screen' &&
width === 'normal' && {
'@media only screen and (min-width: 1280px)': css,
}),
...(media === 'screen' &&
width === 'narrow' && {
'@media only screen and (max-width: 1279px)': css,
}),
};
},
};
const Box = ({ children, className }) => {
return jsx("div", { className: className, children: children });
};
const FLEX_DEFAULTS = {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
'& > *': {
flex: 1,
width: '100%',
height: '100%',
},
};
const FLEX = {
ROW: {
...FLEX_DEFAULTS,
flexDirection: 'row',
},
COLUMN: {
...FLEX_DEFAULTS,
flexDirection: 'column',
},
};
const FlexRow = (props) => jsx(Box, { ...props, css: FLEX.ROW });
const FlexColumn = (props) => (jsx(Box, { ...props, css: FLEX.COLUMN }));
const Link = ({ to, type = 'url', external = true, nofollow = true, children, className, }) => {
let link;
let text;
const rel = [];
switch (type) {
case 'url':
link = /^http/i.test(to) ? to : `https://${to.replace(/^\/\//, '')}`;
text = to.replace(/^(?:https?:)?\/\/(?:www\.)?/, '');
if (external) {
rel.push('external');
}
if (nofollow) {
rel.push('nofollow');
}
break;
case 'tel':
link = `tel:${to
.replace(/^(\+\d{2})\s+\(0\)/, '$1')
.replace(/[^0-9+]/g, '')}`;
text = to.replace(/^tel:/i, '');
break;
case 'mail':
link = /^mailto/i.test(to) ? to : `mailto:${to}`;
text = to.replace(/^mailto:/i, '');
break;
}
return (jsx("a", { href: link, ...(rel.length && { rel: rel.join(' ') }), className: className, css: (theme) => ({
textDecoration: 'none',
color: theme.text.color.link,
'&:hover': {
color: theme.text.color.linkHover,
},
}), children: children || text }));
};
const Paragraph = ({ children, className }) => {
if (!children) {
return null;
}
return jsx("p", { className: className, children: children });
};
const DEFAULT_LOCATION_FORMATS = [
'{{city}}, {{region}}',
'{{city}}, {{countryNameAlias}}',
'{{city}}, {{countryNameOfficial}}',
'{{city}}, {{countryCode}}',
'{{city}}',
];
const Contact = ({ location, phone, email, className }) => {
const configLocationFormat = useConfig('format')?.location ?? [];
const formats = asArray(configLocationFormat).concat(...DEFAULT_LOCATION_FORMATS);
const countryName = useLocale('countryName');
const enhancedLocation = {
...location,
...countryName(location?.countryCode),
};
let locationText;
while (formats.length && !locationText) {
locationText = interpolate(formats.shift(), enhancedLocation);
}
return (jsxs("address", { className: className, css: { textAlign: 'right' }, children: [locationText &&
locationText
.split(/\\n|\n/g)
.map((location, i) => jsx("div", { children: location }, i)), phone && jsx(Link, { type: "tel", to: phone, css: { display: 'block' } }), email && (jsx(Link, { type: "mail", to: email, css: { display: 'block' } }))] }));
};
const Section = ({ label, children }) => {
return (jsxs("section", { css: (theme) => ({
...FLEX.ROW,
borderTopWidth: '2px',
borderTopStyle: 'solid',
borderTopColor: theme.spacing.color.divider,
paddingTop: '1rem',
paddingBottom: '1rem',
}), children: [jsx("h2", { css: (theme) => ({
color: theme.text.color.subTitle,
fontSize: '1.3rem',
fontWeight: 'bold',
letterSpacing: '-0.03125rem',
paddingTop: '1rem',
paddingBottom: '1.2rem',
margin: 0,
alignSelf: 'start',
}), children: label }), jsx("div", { css: {
flex: 3,
alignSelf: 'start',
}, children: children })] }));
};
const Date$1 = ({ date, className }) => {
const configDateFormat = useConfig('format')?.date;
const formatDate = useLocale('formatDate');
const fmt = formatDate(date, configDateFormat);
if (!fmt) {
return null;
}
return (jsx("div", { className: className, css: (theme) => ({
fontStyle: 'italic',
whiteSpace: 'nowrap',
color: theme.text.color.tertiary,
}), children: jsx("time", { dateTime: fmt.dateISO, children: fmt.date }) }));
};
/**
* Hides whitespace characters in the DOM for elements injecting pseudo content that
* otherwise results in select and copying of text not including spaces as expected.
*/
const ZeroWidthSpace = () => {
return (jsx("span", { css: (theme) => ({
flex: 0,
width: '1px',
height: '1px',
marginLeft: '-1px',
marginBottom: '-1px',
overflow: 'hidden',
display: 'inline-block',
verticalAlign: 'baseline',
...theme.when('print', {
display: 'none',
}),
}), dangerouslySetInnerHTML: { __html: ' ' } }));
};
const DateRange = ({ startDate, endDate, className }) => {
const i18n = useLocale('i18n');
const configDateFormat = useConfig('format')?.date;
const formatDateRange = useLocale('formatDateRange');
const fmt = formatDateRange({
startDate,
endDate,
format: configDateFormat,
});
if (!fmt) {
return null;
}
return (jsx("div", { className: className, css: (theme) => ({
fontStyle: 'italic',
whiteSpace: 'nowrap',
color: theme.text.color.tertiary,
'& > time > time:last-of-type::before': {
content: '"→"',
margin: '0 .5rem',
},
}), children: jsxs("time", { dateTime: fmt.durationISO, children: [jsx("time", { dateTime: fmt.startDateISO, children: fmt.startDate }), jsx(ZeroWidthSpace, {}), jsx("time", { dateTime: fmt.endDateISO, children: endDate ? fmt.endDate : i18n('component.date.now') })] }) }));
};
const renderLabel = (label, style) => {
if (!label) {
return null;
}
const styles = {
primary: {
fontWeight: 'bold',
},
secondary: {
'&::before': {
content: '"—"',
margin: '0 .5rem',
},
},
}[style];
switch (typeof label) {
case 'string':
return style === 'primary' ? (jsx("strong", { css: styles, children: label })) : (jsxs("span", { css: styles, children: [jsx(ZeroWidthSpace, {}), label] }));
// Link Props
case 'object':
return style === 'primary' ? (jsx("strong", { css: styles, children: jsx(Link, { ...label }) })) : (jsxs("span", { css: styles, children: [jsx(ZeroWidthSpace, {}), jsx(Link, { ...label })] }));
default:
return null;
}
};
const SubSection = ({ label, children, date, startDate, endDate, }) => {
const [primaryLabel, secondaryLabel] = Array.isArray(label)
? label
: [label];
return (jsxs("section", { css: (theme) => ({
...FLEX.COLUMN,
'&:not(:first-of-type)': {
borderTopWidth: '2px',
borderTopStyle: 'solid',
borderTopColor: theme.spacing.color.divider,
paddingTop: '1rem',
},
'&:not(:last-of-type)': {
paddingBottom: '1rem',
},
}), children: [jsxs(FlexRow, { css: {
marginBottom: '1rem',
}, children: [jsxs("h3", { css: {
fontSize: '1rem',
alignSelf: 'start',
}, children: [renderLabel(primaryLabel, 'primary'), renderLabel(secondaryLabel, 'secondary')] }), startDate && (jsx(DateRange, { css: { flex: 0, alignSelf: 'start' }, startDate: startDate, endDate: endDate })), date && !startDate && (jsx(Date$1, { css: { flex: 0, alignSelf: 'start' }, date: date }))] }), children] }));
};
const useSvgIcons = (entries) => {
const svgIcons = useMemo(() => {
return entries.reduce((acc, { icon }) => {
const key = icon && normaliseTag(icon);
const svg = key && getIconSvg(key);
if (svg) {
acc[key] = svg;
}
return acc;
}, {});
}, [entries]);
const hasIcons = Boolean(svgIcons && Object.keys(svgIcons).length);
const getIcon = (key) => (key && svgIcons[normaliseTag(key)]) ?? null;
return [hasIcons, getIcon];
};
const SimpleEntries = ({ showUrl, entries, className }) => {
const [hasIcons, getIcon] = useSvgIcons(entries);
return (jsx("ul", { className: className, children: entries.map(({ icon, title, label, url, children = null }, i) => (jsxs("li", { css: (theme) => {
const svg = getIcon(icon);
return {
'&:not(:last-of-type)': {
marginBottom: '.6rem',
},
paddingLeft: hasIcons
? 'calc(1.25rem + 0.8rem)'
: 0,
...(svg && {
position: 'relative',
'&::before': {
display: 'block',
position: 'absolute',
top: '0.15rem',
left: 0,
width: '1.25rem',
height: '100%',
backgroundColor: theme.text.color.tertiary,
content: `""`,
maskImage: `url(data:image/svg+xml;utf8,${encodeURIComponent(svg)})`,
maskRepeat: 'no-repeat',
},
}),
};
}, children: [jsxs("div", { children: [title && (jsxs("strong", { css: {
fontWeight: 'bold',
}, children: [title, (label || url) && jsx(ZeroWidthSpace, {})] })), !url && label && (jsx("span", { css: {
'&::before': {
content: '"—"',
margin: '0 .5rem',
},
}, children: label })), url && (jsx(Link, { css: {
'&::before': {
content: '"—"',
margin: '0 .5rem',
},
}, to: url, children: showUrl ? undefined : label }))] }), children && jsx("div", { children: children })] }, i))) }));
};
const renderer = ({ name, keywords }, i) => {
return (jsxs(FlexRow, { css: {
justifyContent: 'start',
flexWrap: 'wrap',
whiteSpace: 'nowrap',
'&:not(:last-of-type)': {
marginBottom: '1rem',
},
'& > *': {
flexGrow: 0,
},
}, children: [keywords?.map((keyword, i) => (jsx("dt", { css: (theme) => ({
marginLeft: '.5rem',
'&::before': {
content: '"-"',
marginRight: '.5rem',
color: theme.text.color.tertiary,
},
}), children: keyword }, i))), jsx("dd", { css: { order: -1, fontWeight: 'bold' }, children: name })] }, i));
};
const KeywordEntries = ({ entries, className }) => {
return jsx("dl", { className: className, children: entries.map(renderer) });
};
const Highlights = ({ children, className }) => {
if (!children) {
return null;
}
return (jsx("ul", { className: className, css: (theme) => ({
'& > li': {
position: 'relative',
paddingLeft: '2.5rem',
},
'& > li::before': {
display: 'block',
position: 'absolute',
top: 0,
left: 0,
content: '"\u2022"',
fontWeight: 'bold',
color: theme.text.color.tertiary,
margin: '0 1rem',
},
'& > li:not(:last-of-type)': {
marginBottom: '.4rem',
},
}), children: children.map((highlight, i) => (jsx("li", { children: highlight }, i))) }));
};
const PageBreak = ({ enable = true }) => {
return !enable ? null : (jsx("div", { css: (theme) => ({
...theme.when('print', {
breakBefore: enable ? 'page' : 'initial',
}),
}) }));
};
const Title = ({ name, label }) => {
return (jsxs(FlexColumn, { children: [name && (jsx("h1", { css: (theme) => ({
fontSize: '2.8rem',
lineHeight: '1.2',
fontWeight: 'bold',
color: theme.text.color.title,
...theme.when('print', {
marginTop: !label ? '4rem' : '2rem',
marginBottom: !label ? '4rem' : 0,
}),
...theme.when(['screen', 'normal'], {
marginTop: !label ? '4rem' : '2rem',
marginBottom: !label ? '4rem' : 0,
}),
...theme.when(['screen', 'narrow'], {
marginTop: !label ? '2rem' : '1rem',
marginBottom: !label ? '2rem' : 0,
}),
}), children: name })), label && (jsx("h2", { css: (theme) => ({
fontSize: '2rem',
lineHeight: '1.1',
color: theme.text.color.tertiary,
...theme.when('print', {
marginTop: !name ? '4rem' : 0,
marginBottom: !name ? '4rem' : '2rem',
}),
...theme.when(['screen', 'normal'], {
marginTop: !name ? '4rem' : 0,
marginBottom: !name ? '4rem' : '2rem',
}),
...theme.when(['screen', 'narrow'], {
marginTop: !name ? '2rem' : 0,
marginBottom: !name ? '2rem' : '1rem',
}),
}), children: label }))] }));
};
const Summary = ({ summary }) => {
return (jsx(Paragraph, { css: (theme) => ({
fontFamily: theme.text.font.secondary,
textAlign: 'justify',
fontStyle: 'italic',
fontSize: '1.2rem',
letterSpacing: '0.03125rem',
color: theme.text.color.secondary,
...theme.when('print', {
padding: '2rem 6rem 2rem 7rem',
}),
...theme.when(['screen', 'normal'], {
padding: '2rem 6rem 2rem 7rem',
}),
...theme.when(['screen', 'narrow'], {
padding: '1rem 3rem 2rem 3.5rem',
}),
}), children: summary || '' }));
};
const Avatar = ({ name, image, gravatar }) => {
const i18n = useLocale('i18n');
const config = useConfig('intro');
const showAvatar = !config?.avatar?.hidden;
if (!showAvatar || !name || !(image || gravatar)) {
return null;
}
return (jsx("div", { css: {
alignSelf: 'start',
maxWidth: '150px',
...(config?.avatar?.align === 'left'
? {
marginRight: '3rem',
order: -1,
}
: {
marginLeft: '2rem',
}),
}, children: jsx("img", { css: {
display: 'block',
width: '100%',
height: 'auto',
borderRadius: '4px',
}, src: image || gravatar, alt: i18n(image ? 'component.picture.alt' : 'component.avatar.alt', {
name,
}) }) }));
};
const Basics = () => {
const { basics } = useResume();
if (!basics) {
return null;
}
return (jsxs(Fragment, { children: [jsxs(FlexRow, { children: [jsx(Title, { name: basics.name, label: basics.label }), jsx(Contact, { phone: basics.phone, email: basics.email, location: basics.location, css: { alignSelf: 'start' } }), jsx(Avatar, { name: basics.name, image: basics.image, gravatar: basics.gravatar })] }), jsx(Summary, { summary: basics.summary })] }));
};
const StackOverflowMeta = ({ answers, tags }) => {
const i18n = useLocale('i18n');
if (!answers || !tags || answers <= 0 || tags.length <= 0) {
return null;
}
return (jsx("em", { css: {
fontStyle: 'italic',
marginTop: '.3rem',
}, children: i18n('section.profiles.stackoverflow-activity', {
answers,
tags: tags.join(', '),
}) }));
};
const useNetworkDetail = () => {
const i18n = useLocale('i18n');
const basics = useResume().basics;
const skills = useResume().skills;
return useMemo(() => (network) => {
const key = network && normaliseTag(network);
let networkDetail;
if (key === TAG_STACKOVERFLOW) {
const meta = basics?.stackOverflow;
if (!meta || !skills) {
return;
}
const relevantKeywords = skills.reduce((acc, { keywords = [] }) => {
acc.push(...keywords.map(normaliseTag));
return acc;
}, []);
const relevantTags = meta.activeTags.filter((tag) => relevantKeywords.includes(tag));
networkDetail = (jsx(StackOverflowMeta, { answers: meta.answersTotal, tags: relevantTags }));
}
return networkDetail;
}, [i18n, basics, skills]);
};
const BasicsProfiles = () => {
const i18n = useLocale('i18n');
const networkDetail = useNetworkDetail();
const profiles = useResume().basics?.profiles || [];
const useableProfiles = profiles.filter(filterAny(filterPopulated('network', 'username'), filterPopulated('network', 'url')));
if (!useableProfiles) {
return null;
}
return (jsx(Section, { label: i18n('section.profiles.title'), children: jsx(SimpleEntries, { showUrl: true, entries: useableProfiles.map(({ network, username: label, url }) => ({
title: network,
icon: network,
label,
url,
children: networkDetail(network),
})), css: {
marginBottom: '1rem',
} }) }));
};
const Skills = () => {
const i18n = useLocale('i18n');
const { skills } = useResume();
const useableSkills = skills?.filter(filterPopulated('name', 'keywords'));
if (!useableSkills) {
return null;
}
return (jsx(Section, { label: i18n('section.skills.title'), children: jsx(KeywordEntries, { css: { marginBottom: '1rem' }, entries: useableSkills }) }));
};
const Work = () => {
const i18n = useLocale('i18n');
const { work } = useResume();
const useableWork = work?.filter(filterPopulated('name', 'position', 'startDate'));
if (!useableWork) {
return null;
}
return (jsx(Section, { label: i18n('section.work.title'), children: useableWork.map((item, i) => (jsxs(SubSection, { startDate: item.startDate, endDate: item.endDate, label: [
item.position,
!item.url
? item.name
: { to: item.url, children: item.name },
], children: [jsx(Paragraph, { css: { marginBottom: '1rem' }, children: item.summary }), jsx(Highlights, { css: { marginBottom: '1rem ' }, children: item.highlights })] }, i))) }));
};
const Volunteer = () => {
const i18n = useLocale('i18n');
const { volunteer } = useResume();
const useableVolunteer = volunteer?.filter(filterPopulated('organization', 'position', 'startDate'));
if (!useableVolunteer) {
return null;
}
return (jsx(Section, { label: i18n('section.volunteer.title'), children: useableVolunteer.map((item, i) => (jsxs(SubSection, { startDate: item.startDate, endDate: item.endDate, label: [
item.position,
!item.url
? item.organization
: { to: item.url, children: item.organization },
], children: [jsx(Paragraph, { css: { marginBottom: '1rem' }, children: item.summary }), jsx(Highlights, { css: { marginBottom: '1rem ' }, children: item.highlights })] }, i))) }));
};
const Projects = () => {
const i18n = useLocale('i18n');
const { projects } = useResume();
const useableProjects = projects?.filter(filterPopulated('name', 'description', 'startDate'));
if (!useableProjects) {
return null;
}
return (jsx(Section, { label: i18n('section.projects.title'), children: useableProjects.map((item, i) => (jsxs(SubSection, { startDate: item.startDate, endDate: item.endDate, label: [item.name, item.url && { to: item.url }], children: [jsx(Paragraph, { css: { marginBottom: '1rem' }, children: item.description }), jsx(Highlights, { css: { marginBottom: '1rem ' }, children: item.highlights })] }, i))) }));
};
const Education = () => {
const i18n = useLocale('i18n');
const { education } = useResume();
const useableEducation = education?.filter(filterPopulated('area', 'institution', 'startDate'));
if (!useableEducation) {
return null;
}
return (jsx(Section, { label: i18n('section.education.title'), children: useableEducation.map((item, i) => (jsx(SubSection, { startDate: item.startDate, endDate: item.endDate, label: [
item.studyType
? `${item.studyType} ${item.area}`
: item.area,
!item.url
? item.institution
: { to: item.url, children: item.institution },
], children: jsx(Highlights, { css: { marginBottom: '1rem' }, children: item.courses }) }, i))) }));
};
const Awards = () => {
const i18n = useLocale('i18n');
const { awards } = useResume();
const useableAwards = awards?.filter(filterPopulated('title', 'date'));
if (!useableAwards) {
return null;
}
return (jsx(Section, { label: i18n('section.awards.title'), children: useableAwards.map((item, i) => (jsx(SubSection, { date: item.date, label: [item.title, item.awarder], children: jsx(Paragraph, { css: { marginBottom: '1rem' }, children: item.summary }) }, i))) }));
};
const Publications = () => {
const i18n = useLocale('i18n');
const { publications } = useResume();
const useablePublications = publications?.filter(filterPopulated('name', 'releaseDate'));
if (!useablePublications) {
return null;
}
return (jsx(Section, { label: i18n('section.publications.title'), children: useablePublications.map((item, i) => (jsx(SubSection, { date: item.releaseDate, label: [
item.name,
!item.url
? item.publisher
: { to: item.url, children: item.publisher },
], children: jsx(Paragraph, { css: { marginBottom: '1rem' }, children: item.summary }) }, i))) }));
};
const Languages = () => {
const i18n = useLocale('i18n');
const { languages } = useResume();
const useableLanguages = languages?.filter(filterPopulated('language', 'fluency'));
if (!useableLanguages) {
return null;
}
return (jsx(Section, { label: i18n('section.languages.title'), children: jsx(SimpleEntries, { entries: useableLanguages.map(({ language: title, fluency: label }) => ({ title, label })), css: {
marginBottom: '1rem',
} }) }));
};
const Interests = () => {
const i18n = useLocale('i18n');
const { interests } = useResume();
const useableInterest = interests?.filter(filterPopulated('name', 'keywords'));
if (!useableInterest) {
return null;
}
return (jsx(Section, { label: i18n('section.interests.title'), children: jsx(KeywordEntries, { css: { marginBottom: '1rem' }, entries: useableInterest }) }));
};
const References = () => {
const i18n = useLocale('i18n');
const { references } = useResume();
const useableReferences = references?.filter(filterPopulated('name', 'reference'));
if (!useableReferences) {
return null;
}
return (jsx(Section, { label: i18n('section.references.title'), children: useableReferences.map((item, i) => (jsx(SubSection, { label: item.name, children: jsx(Paragraph, { css: (theme) => ({
marginBottom: '1rem',
color: theme.text.color.secondary,
fontFamily: theme.text.font.secondary,
fontStyle: 'italic',
'&::before': {
content: '"“"',
display: 'inline-block',
paddingRight: '.2rem',
},
'&::after': {
content: '"”"',
display: 'inline-block',
paddingLeft: '.2rem',
},
}), children: item.reference }) }, i))) }));
};
const SECTIONS = {
skills: Skills,
work: Work,
volunteer: Volunteer,
projects: Projects,
education: Education,
awards: Awards,
publications: Publications,
languages: Languages,
interests: Interests,
profiles: BasicsProfiles,
references: References,
};
// [name, Component, pageBreak, order]
const useSections = () => {
const config = useConfig('section');
const sectionConfig = (name) => config?.[name];
return useMemo(() => {
return Object.entries(SECTIONS)
.filter(([name]) => !sectionConfig(name)?.hidden)
.map(([name, Component], i) => [
name,
Component,
sectionConfig(name)?.break ?? false,
sectionConfig(name)?.order ?? i + 1,
])
.sort(([, , , aOrder], [, , , bOrder]) => aOrder - bOrder);
}, [config]);
};
const Pdf = () => {
const sections = useSections();
return (jsxs(FlexColumn, { css: (theme) => ({
fontFamily: theme.text.font.primary,
color: theme.text.color.primary,
lineHeight: '1.5',
...theme.when(['screen', 'normal'], {
padding: '4rem 4rem 4rem 6rem',
}),
...theme.when(['screen', 'narrow'], {
padding: '2rem 2rem 2rem 3rem',
}),
}), children: [jsx("header", { children: jsx(Basics, {}) }), jsx("main", { children: sections.map(([name, Component, pageBreak]) => (jsxs(Fragment$1, { children: [jsx(PageBreak, { enable: pageBreak }), jsx(Component, {})] }, name))) })] }));
};
const cssReset = {
html: {
fontSize: '16px',
boxSizing: 'border-box',
},
'*, *::before, *::after': {
boxSizing: 'inherit',
},
'body, h1, h2, h3, h4, h5, h6, p, ol, ul, dl, dt, dd, pre': {
margin: 0,
padding: 0,
fontWeight: 'normal',
},
'ol, ul': {
listStyle: 'none',
},
img: {
maxWidth: '100%',
height: 'auto',
},
'address, em, string': {
fontStyle: 'normal',
},
};
const cssPageContext = {
...theme.when('print', {
'@page': {
size: 'Letter',
margin: '.444444444in .444444444in .444444444in .666666667in',
},
}),
body: {
...theme.when('screen', {
backgroundColor: '#f5f5f5',
}),
...theme.when(['screen', 'normal'], {
padding: '1rem',
}),
...theme.when(['screen', 'narrow'], {
padding: '.5rem',
}),
},
'#root': {
...theme.when('print', {
// 8.5in "Letter" format (minus margins) then multiplied by DPI
// - apparently constant cross platform? - not sure where it is
// derived from - found by trial and error... :-/
width: 'calc((8.5 - .444444444 - .666666667) * 144px)',
}),
...theme.when('screen', {
margin: '0 auto',
boxShadow: '4px 4px 8px rgba(0, 0, 0, 0.25)',
backgroundColor: '#ffffff',
}),
...theme.when(['screen', 'normal'], {
width: 'calc(1280px - 2 * 1rem)',
}),
...theme.when(['screen', 'narrow'], {
width: '1000px',
}),
},
};
const Root = () => {
return (jsxs(ThemeProvider, { theme: theme, children: [jsx(Global, { styles: cssReset }), jsx(Global, { styles: cssPageContext }), jsx(Pdf, {})] }));
};
const GRAVATAR_API = 'https://gravatar.com/avatar';
const findImage = (image) => {
const sanitizedImage = image && image.trim();
if (!sanitizedImage) {
return Promise.resolve('');
}
return axios
.head(sanitizedImage)
.then(() => sanitizedImage)
.catch(() => '');
};
const findGravatar = (email) => {
const sanitizedEmail = email && email.trim();
if (!sanitizedEmail) {
return Promise.resolve('');
}
const hash = md5(sanitizedEmail);
const query = new URLSearchParams({
size: '200',
d: '404',
});
const maybeGravatar = `${GRAVATAR_API}/${hash}?${query}`;
query.delete('d');
const definatelyGravatar = `${GRAVATAR_API}/${hash}?${query}`;
return axios
.head(maybeGravatar)
.then(() => definatelyGravatar)
.catch(() => '');
};
const profileImage = async (resume) => {
const [image, gravatar] = await Promise.all([
findImage(resume.basics?.image),
findGravatar(resume.basics?.email),
]);
return (draft) => {
if (!draft.basics) {
return;
}
draft.basics.image = image;
draft.basics.gravatar = gravatar;
};
};
const API = 'https://api.stackexchange.com/2.3';
const getCredentials = () => {
const { STACK_EXCHANGE_API_KEY: key, STACK_EXCHANGE_ACCESS_TOKEN: access_token, } = process.env;
if (!key) {
return null;
}
if (['anon', 'anonymous'].includes(key.toLowerCase())) {
return {};
}
return {
key,
...(access_token && {
access_token,
}),
};
};
const findProfile = (profiles) => profiles?.find(({ network }) => network && normaliseTag(network) === TAG_STACKOVERFLOW);
const findUserId = (profiles) => {
const profile = findProfile(profiles);
return (profile?.url &&
String(profile.url).match(/stackoverflow\.com\/users\/(\d+)/i)?.[1]);
};
const fetchTopTags = (id, credentials) => {
const query = new URLSearchParams({
order: 'desc',
sort: 'popular',
site: TAG_STACKOVERFLOW,
...credentials,
});
return axios
.get(`${API}/users/${id}/top-tags?${query}`)
.then(({ data }) => data?.items ?? [])
.catch(() => []);
};
const fetchAnswersTotal = (id, credentials) => {
const query = new URLSearchParams({
filter: 'total',
site: TAG_STACKOVERFLOW,
...credentials,
});
return axios
.get(`${API}/users/${id}/answers?${query}`)
.then(({ data }) => data?.total ?? 0)
.catch(() => 0);
};
const stackOverflow = async (resume) => {
const credentials = getCredentials();
const userId = findUserId(resume.basics?.profiles);
if (!credentials || !userId) {
return;
}
const [answersTotal, topTags] = await Promise.all([
fetchAnswersTotal(userId, credentials),
fetchTopTags(userId, credentials),
]);
return (draft) => {
if (!draft.basics) {
return;
}
draft.basics.stackOverflow = {
answersTotal,
activeTags: topTags.map(({ tag_name }) => normaliseTag(tag_name)),
};
};
};
const resumeMiddleware = compose(profileImage, stackOverflow);
const render = async (resume) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { name: pkgName, version: pkgVersion } = require('../package.json');
const enhancedResume = await resumeMiddleware(resume);
const locale = await getLocale(resume.meta?.[THEME_NAME]?.locale);
return template({
resume,
meta: {
generator: `${pkgName}@${pkgVersion}`,
},
body: renderToString(jsx(AppContext.Provider, { value: { resume: enhancedResume, locale }, children: jsx(Root, {}) })),
});
};
const pdfRenderOptions = {
mediaType: 'print',
};
export { pdfRenderOptions, render };