UNPKG

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
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: '&nbsp;' } })); }; 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 };