Customize resume data, themes, export script, and PDF outputs

- Update resume/data.yml and English strings
- Adjust resume.vue, options, and theme components
- Change export flow in scripts/export.js and package.json
- Refresh PDF artifacts; add static/green.pdf; trim unused pdf/

Made-with: Cursor
This commit is contained in:
ilia 2026-03-25 10:36:38 -04:00
parent 29e753cd84
commit c8a49018b1
41 changed files with 1009 additions and 201 deletions

View File

@ -9,11 +9,13 @@
"url": "git+https://github.com/salomonelli/best-resume-ever.git"
},
"scripts": {
"docs": "node build/build.js",
"docs": "node --openssl-legacy-provider build/build.js",
"docs:serve": "cd docs/ && ws --port 8080 --rewrite '/best-resume-ever/* -> /$1'",
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"dev": "node --openssl-legacy-provider build/dev-server.js",
"start": "node --openssl-legacy-provider build/dev-server.js",
"pdf": "node scripts/export.js",
"pdf:green": "node scripts/export.js green",
"export:green": "concurrently \"npm run dev\" \"node scripts/export.js green\" --success first --kill-others --raw",
"preview": "node scripts/preview.js",
"test:deleteFiles": "node test/scripts/deleteFiles.js",
"test:cafe": "testcafe chromium test/",
@ -84,7 +86,7 @@
"pdf-image": "2.0.0",
"postcss": "7.0.4",
"postcss-cssnext": "3.1.0",
"puppeteer": "1.8.0",
"puppeteer": "^22.15.0",
"rename": "1.0.4",
"request": "2.88.0",
"request-promise": "4.2.2",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,98 +1,195 @@
/* #*/ export const PERSON = `
# Any fields left unchanged, please delete so your resume is fully yours!
# Experience: timeperiod = dates only; location = separate line. description: | = one bullet per line.
# Optional: remote: true | false | hybrid; employment: contract | full-time | co-op (icons + legend on green/purple).
# Optional contact: github_profile = public GitHub.com username (add if you use both Forge + GitHub).
name:
first: John
first: ILIA
middle:
last: Doe
about: Hi, my name is John Doe. I'm just about the most boring type of person you could
possibly imagine. I like collecting leaves from the tree in my back yard and documenting
each time I eat a peanut that is non-uniform. I am not a robot. Please hire me.
position: Software Developer
last: DOBKIN
about: "Driven software engineer with 20+ years of experience spanning product, platform, and industrial test automation — from global audit and financial systems (CaseWare, MNP, JazzIt) to modern web delivery for startups and enterprises alike. Builds and maintains a self-hosted infrastructure lab (Proxmox, Ansible, Caddy, CI runners) that mirrors production-grade DevOps practices. Seeking roles where I can provide testing guidance, strengthen CI/CD operations, and collaborate with teams to optimize product delivery."
core_strengths: "E2E test automation (Cypress, Playwright, Selenium), BDD (SpecFlow, Cucumber), accessibility (AODA/WCAG), observability (Grafana, Prometheus), IaC (Terraform), API and performance testing (Postman, Artillery), and reusable frameworks built for team adoption."
position: Software Development Engineer in Test
birth:
year: 1990
location: New York
# you may add more experiences by duplicating the template
year:
location: Thornhill, Ontario, Canada
experience:
- company: Company A
position: Developer
timeperiod: since January 2016
description: Programming and watching cute cat videos.
website: https://example.com
- company: Niyasoft Canada Inc.
position: Test Automation Engineer
timeperiod: August 2023 - April 2026
remote: true
employment: full-time
location: Vaughan, Ontario, Canada
description: |
• Test Automation Engineer at an iGaming technology company specializing in online casino platform delivery; contributed to regulated, operator-facing products in an Agile environment with front-end and back-end teams.
• Built and maintained Playwright UI automation, API and integration tests, and Artillery performance suites; focused coverage on high-risk journeys including payments, wallet and cashier flows, game and lobby integrations, and supporting back-office capabilities.
• Validated responsible gaming and player-protection behavior: deposit, loss, and session limits; self-exclusion and cooling-off; reality checks and safer-gambling messaging; ensured flows behaved correctly for compliance and operator policy.
• Exercised compliance-sensitive scenarios such as market and geo-eligibility, age-gating touchpoints, restricted jurisdictions, and audit-friendly logging and traceability expected in licensed wagering markets.
• Managed GitHub Actions CI/CD for regression, functional, component, and smoke stages; used pull requests and code review daily to keep releases predictable for a high-availability real-money platform.
• Monitored API behavior and reliability with GCP, Prometheus metrics, and broader observability, logging, and alerting; validated PostgreSQL-backed data and integrations while keeping automation aligned with GitHub-driven workflows.
- company: Company B
position: Frontend Developer
timeperiod: January 2015 - December 2015
description: Fulfillment of extremely important tasks.
- company: RIOS Canada
position: Software Development Engineer in Test
timeperiod: June 2022 - July 2023
remote: true
employment: contract
location: Toronto, Ontario, Canada
description: |
• Integrated end-to-end Cypress automation from the ground up for GUI and API testing across critical product flows; led training for engineering.
• Conducted AODA accessibility work: alt text, keyboard navigation, color contrast; scaled regression across web and mobile via Bitbucket CI/CD.
• Used Ansible to automate provisioning for repeatable test environments; partnered with engineers and product on triage and quality gates.
• Partnered with software engineers and product on defect triage, test reporting, and pragmatic quality gates.
- company: Company C
position: Trainee
timeperiod: March 2014 - December 2014
description: Making coffee and baking cookies.
- company: Attabotics
position: QA Automation Developer
timeperiod: September 2021 - May 2022
remote: true
employment: contract
location: Calgary, Alberta, Canada
description: |
• Wrote Gherkin/SpecFlow scenarios with C# step definitions; sustained 3,500+ automated scenarios in a .NET / Azure environment.
• Practiced left-shift QA within a large Agile team: testers engaged early in design and development.
• Used Docker for local test environments and SQL Server for test data validation and traceability.
education:
- degree: Master of Arts
timeperiod: March 2012 - December 2013
description: Major in Hacking and Computer Penetration, University A, New York, USA.
website: https://example.com
- company: Levkin Inc.
position: Senior Software Developer
timeperiod: October 2020 - August 2021
remote: true
employment: contract
location: Vaughan, Ontario, Canada
description: |
• Built reusable Playwright testing building blocks with deterministic patterns, reducing flakiness and eliminating reliance on arbitrary sleeps or built-in waits.
• Audited and refactored legacy test and UI code toward current standards; documented testing strategy and shared knowledge across the team.
• Optimized GitLab CI/CD pipelines for speed and reliability; piped test and pipeline metrics into Grafana dashboards for release visibility.
• Used GitLab for repositories, merge requests, and code review as part of day-to-day development and collaboration.
• Provisioned AWS environments with Terraform, validated them end-to-end, and promoted changes to dev following the team's standard release procedure.
- degree: Bachelor of Science
timeperiod: March 2009 - December 2011
description: Major in Engineering, University B, Los Angeles, USA.
- company: Accountants Templates Inc.
position: Senior Software Developer
timeperiod: August 2019 - August 2020
remote: true
employment: contract
location: Calgary, Alberta, Canada
description: |
• Owned CaseWare/CaseView template delivery: compliance updates, standards-driven releases, and documentation for internal and client use.
• Reviewed software for improvements and implemented recommendations; collaborated with support on reported issues.
• Streamlined build and packaging workflows, removing approximately eight hours of manual effort per release cycle.
# skill level goes from 0 to 100
- company: MNP LLP
position: Senior Application Developer
timeperiod: August 2017 - June 2019
remote: true
employment: full-time
location: Toronto, Ontario, Canada
description: |
• Designed, developed, and maintained software integrating with CaseWare/CaseView; extended functionality with JavaScript where specifications required.
• Delivered and maintained C# / .NET / .NET Core applications from technical specification through production.
• Assisted automation testers with Selenium tests, Jenkins triggers, Cucumber reporting, and JIRA summaries for management.
• Contributed automation strategy alongside hands-on Selenium/Cucumber work and Azure DevOps for planning, repos, and deployments.
- company: CaseWare International Inc.
position: Software Developer
timeperiod: August 2006 - June 2017
remote: hybrid
employment: full-time
location: Toronto, Ontario, Canada
description: |
• Implemented features, resolved defects, and maintained financial and audit systems; supported distributors and clients.
• Delivered client templates with JavaScript, HTML, YUI, jQuery, JSON, and CSS at global scale.
• Designed and executed automated validation with SilkTest; mentored junior developers, built reusable JS libraries, and used Agile Scrum, Jira, and Git.
- company: ROLI Consulting
position: Web/Application Developer
timeperiod: January 2001 - July 2012
remote: true
employment: full-time
location: Vaughan, Ontario, Canada
description: |
• Developed and maintained a voice broadcasting system and text-messaging service using Python and the Twilio API for commercial clients.
• Designed and maintained websites across multiple stacks, including WordPress; provided general technical consulting for nonprofits and SMBs.
- company: Earlier Career
timeperiod: May 2005 - August 2006
sub_companies:
- name: Kaboose Inc.
remote: false
employment: contract
- name: Coutts Information Services
remote: false
employment: full-time
- name: EDS / Scotiabank
remote: false
employment: co-op
description: |
• QA automation with QTP/Quality Center, cross-browser and end-to-end testing, and UAT support (Kaboose); Java/J2EE development (Coutts); Informatica ETL co-op on AIX/DB2 (EDS/Scotiabank).
education: []
# Skills: each line is one grid cell in green template (two columns).
skills:
- name: HTML5
level: 99
- name: CSS3
level: 95
- name: JavaScript
level: 97
- name: Node.js
level: 93
- name: Angular 2
level: 60
- name: TypeScript
- name: "Test automation: Cypress, Playwright, Selenium, SilkTest, mobile & cross-browser E2E, API testing, JUnit, PyTest, TestRail, page object model"
level: 96
- name: "Languages & stacks: TypeScript, JavaScript, C#, Python, Java, .NET & ASP.NET, Node.js, HTML, CSS, Spring Boot"
level: 92
- name: "CI/CD: GitHub Actions, GitHub, GitLab CI, Bitbucket, Jenkins, Azure DevOps, Ansible, self-hosted runners"
level: 92
- name: "BDD: SpecFlow, Cucumber, Gherkin, .NET test stacks"
level: 86
- name: "Cloud & containers: AWS Lambda, Azure, GCP, Google Cloud, Docker"
level: 84
- name: "API & performance: Postman, Artillery, JMeter, REST APIs, Twilio, service testing"
level: 88
- name: "Observability: Grafana, Prometheus, Sentry, DataDog, metrics, logging, pipeline telemetry"
level: 80
- name: ES.Next
level: 70
- name: Docker
level: 99
knowledge: Also proficient in Adobe Photoshop and Illustrator, grew up bilingual
(English and Klingon).
- name: "Accessibility: AODA, WCAG-oriented audits"
level: 80
- name: "Data platforms: PostgreSQL, MySQL, SQL Server, DB2, Informatica, ETL"
level: 78
- name: "Delivery: Agile, Scrum, shift-left QA, Jira, Confluence, cross-functional SDET collaboration"
level: 92
- name: "Domain: CaseWare, CaseView, Crystal Reports, audit & finance software"
level: 76
- name: "Version control & IaC: Git, Terraform, Ansible, infrastructure as code"
level: 88
- name: "Homelab & local AI: Proxmox, Linux, Caddy, TrueNAS, Vaultwarden, SonarQube, n8n, Gitea, DNS, local LLM/GPU inference, privacy-first agents"
level: 78
# Dense keyword line for ATS / AI parsers — mirrors tools and titles in your experience.
knowledge: SDET, software development engineer in test, test automation engineer, QA automation, Cypress, Playwright, Selenium, SpecFlow, Cucumber, Gherkin, BDD, TypeScript, JavaScript, C#, .NET, Node.js, Python, PyTest, Java, Spring Boot, HTML, CSS, REST API, E2E, GitHub, GitLab, Bitbucket, Jenkins, Azure DevOps, Ansible, Terraform, infrastructure as code, CI/CD, continuous integration, Docker, Proxmox, AWS Lambda, Azure, GCP, Google Cloud, Prometheus, Grafana, Sentry, DataDog, Postman, Artillery, JMeter, JUnit, AODA, WCAG, accessibility testing, cross-browser testing, mobile testing, regression testing, smoke testing, functional testing, test strategy, page object model, PostgreSQL, MySQL, SQL Server, DB2, Informatica, ETL, Crystal Reports, TestRail, SilkTest, ASP.NET, Twilio, CaseWare, CaseView, Jira, Confluence, Agile, Scrum, shift-left QA, financial software, Linux, Caddy, TrueNAS, Vaultwarden, SonarQube, n8n, self-hosted CI runners, DNS, domain management, local AI, LLM, GPU inference, privacy-first automation, agentic workflows, email and calendar integration
projects:
- name: best-resume-ever
platform: Vue
timeperiod: February 2016
description: 👔 💼 Build fast 🚀 and easy multiple beautiful resumes and create your best CV ever! Made with Vue and LESS.
url: https://github.com/salomonelli/best-resume-ever
- name: Self-Hosted Infrastructure Lab
description: |
• Built and maintain a Proxmox-based homelab running production-grade services: Gitea, Vaultwarden, Vikunja, Uptime Kuma, Mailcow, Listmonk, n8n, SonarQube, and self-hosted CI runners.
• Manage all provisioning, configuration, and site deployments with Ansible playbooks; use Caddy as a reverse proxy with automatic TLS.
• Maintain several domains and host personal sites and services; administer Linux servers end-to-end including networking, security hardening, and monitoring.
• Operate TrueNAS for networked storage and backups; use Vaultwarden for secure credential and sensitive data management.
# optional, not all resume templates have hobbies included
hobbies:
- name: Video Games
iconClass: fa fa-gamepad
url: https://example.com
- name: Privacy-First Local AI Assistant
description: |
• Building a personalized, tool-using assistant wired into email, calendar, and everyday productivity flows—triage, drafting, scheduling, and repeatable tasks—so it can actually do work instead of stopping at generic chat.
• Driving inference on local GPU hardware with self-hosted models so mail, calendar context, and prompts stay off third-party clouds; architecting for speed, control, and a hard privacy boundary aligned with the homelab stack.
- name: Drawing
iconClass: fa fa-pencil
url: https://example.com
hobbies: []
contributions:
- name: best-resume-ever
description: 👔 💼 Build fast 🚀 and easy multiple beautiful resumes.
url: https://github.com/salomonelli/best-resume-ever
contributions: []
contact:
email: john.doe@email.com
phone: 0123 456789
street: 1234 Broadway
city: New York
website: johndoe.com
github: johnyD
email: idobkin@gmail.com
phone: +1 (647) 987-2792
street:
city: Toronto, Ontario, Canada
website: https://www.linkedin.com/in/idobkin/
website_label: LinkedIn
github: https://git.levkin.ca
github_label: Gitea
personal_site: https://iliadobkin.com
personal_site_label: iliadobkin.com
# Uncomment and set your public github.com username if different from the Forge URL above:
# github_profile: yourusername
# github_profile_label: GitHub
# en, de, fr, pt, ca, cn, it, es, th, pt-br, ru, sv, id, hu, pl, ja, ka, nl, he, zh-tw, lt, ko, el, nb-no
lang: en
`

View File

@ -4,6 +4,8 @@ const path = require('path');
const http = require('http');
const config = require('../config');
const devPort = process.env.PORT || config.dev.port;
const {
interval
} = require('rxjs');
@ -16,7 +18,7 @@ const {
const fetchResponse = () => {
return new Promise((res, rej) => {
try {
const req = http.request(`http://localhost:${config.dev.port}/#/`, response => res(response.statusCode));
const req = http.request(`http://localhost:${devPort}/#/`, response => res(response.statusCode));
req.on('error', (err) => rej(err));
req.end();
} catch (err) {
@ -53,15 +55,35 @@ const convert = async () => {
console.log('Exporting ...');
try {
const fullDirectoryPath = path.join(__dirname, '../pdf/');
const directories = getResumesFromDirectories();
directories.forEach(async (dir) => {
let directories = getResumesFromDirectories();
const resumeFilterRaw = (process.env.EXPORT_RESUME || process.argv[2] || '').trim();
const resumeFilter = resumeFilterRaw.replace(/\.vue$/i, '').toLowerCase();
if (resumeFilter) {
directories = directories.filter(d => d.name.toLowerCase() === resumeFilter);
if (directories.length === 0) {
console.error(
`No resume template "${resumeFilterRaw}". Expected a name like "green" (see src/resumes/*.vue).`
);
process.exit(1);
}
console.log('Resume filter: ' + directories.map(d => d.name).join(', '));
}
for (const dir of directories) {
const browser = await puppeteer.launch({
args: ['--no-sandbox']
headless: true,
args: ['--no-sandbox', '--font-render-hinting=none']
});
const page = await browser.newPage();
await page.goto(`http://localhost:${config.dev.port}/#/resume/` + dir.name, {
waitUntil: 'networkidle2'
await page.goto(`http://localhost:${devPort}/#/resume/` + dir.name, {
waitUntil: 'load',
timeout: 120000
});
try {
await page.evaluate(() => document.fonts.ready);
} catch (_err) {
/* ignore if fonts API missing */
}
await new Promise((r) => setTimeout(r, 300));
if (
!fs.existsSync(fullDirectoryPath)
@ -70,10 +92,11 @@ const convert = async () => {
}
await page.pdf({
path: fullDirectoryPath + dir.name + '.pdf',
format: 'A4'
format: 'A4',
printBackground: true
});
await browser.close();
});
}
} catch (err) {
throw new Error(err);
}

View File

@ -4,6 +4,7 @@ const lang = {
born: 'Born',
bornIn: 'in',
experience: 'Experience',
experienceLegendIntro: 'Key',
education: 'Education',
skills: 'Skills',
projects: 'Projects',

View File

@ -18,7 +18,7 @@ export default Vue.component('resume', {
<style scoped>
.page-inner{
height: 100%;
min-height: 100%;
width: 100%;
}
.page-wrapper {
@ -31,7 +31,7 @@ export default Vue.component('resume', {
}
.resume {
height: 100%;
min-height: 100%;
width: 100%;
}
@ -39,9 +39,10 @@ export default Vue.component('resume', {
background: white;
position: relative;
width: 21cm;
height: 29.68cm;
min-height: 29.68cm;
height: auto;
display: block;
page-break-after: auto;
overflow: hidden;
overflow: visible;
}
</style>

View File

@ -32,7 +32,7 @@
</div>
</div>
<div class="section">
<div v-if="person.education && person.education.length" class="section">
<div class="section-headline">
<i class="section-headline__icon material-icons">school</i>{{ lang.education }}
</div>
@ -149,7 +149,7 @@
<a
v-if="person.contact.website"
class="section-link"
:href="person.contact.website">
:href="contactLinks.website">
<i class="section-link__icon fa fa-globe"></i>{{ person.contact.website }}
</a>

View File

@ -66,7 +66,7 @@
<a
v-if="person.contact.website"
class="section-link"
:href="person.contact.website">
:href="contactLinks.website">
<i class="section-link__icon fa fa-globe"></i>{{ person.contact.website }}
</a>
@ -119,7 +119,7 @@
</div>
</div>
<div class="section">
<div v-if="person.education && person.education.length" class="section">
<div class="section-headline">
<i class="section-headline__icon material-icons">school</i>{{ lang.education }}
</div>

View File

@ -65,7 +65,7 @@
<a
v-if="person.contact.website"
class="section-link link"
:href="person.contact.website">
:href="contactLinks.website">
<i class="section-link__icon fa fa-globe"></i>{{ person.contact.website }}
</a>
@ -119,7 +119,7 @@
</div>
</div>
<div class="section">
<div v-if="person.education && person.education.length" class="section">
<div class="section-headline">
<i class="section-headline__icon material-icons">school</i>{{ lang.education }}
</div>

View File

@ -34,7 +34,7 @@
<div class="social-container">
<a v-if="person.contact.website"
:href="person.contact.website">
:href="contactLinks.website">
<div class="block-marged txt-full-white">
<i class="fa fa-globe contact-icon"></i>
@ -115,7 +115,7 @@
</div>
</div>
<div class="education-section section">
<div v-if="person.education && person.education.length" class="education-section section">
<div class="icon">
<i class="material-icons">school</i>
<span class="section-headline">{{ lang.education }}</span>

View File

@ -8,22 +8,101 @@
<span id="email"><a :href='"mailto:" + person.contact.email'>
<i class="fa fa-envelope" aria-hidden="true"></i> {{person.contact.email}}</a></span>
<span id="phone"><i class='fa fa-phone-square' aria-hidden="true"></i> {{person.contact.phone}}</span>
<span v-if="person.contact.website" id="website"><a :href='person.contact.website'><i class="fa fa-home" aria-hidden="true"></i> {{person.contact.website}}</a></span>
<span v-if="person.contact.github" id="github"><a :href='contactLinks.github'><i class="fa fa-github" aria-hidden="true"></i> {{person.contact.github}}</a></span>
<span v-if="person.contact.website" id="website"><a :href="contactLinks.website"><i class="fa fa-linkedin" aria-hidden="true"></i> {{ person.contact.website_label || person.contact.website }}</a></span>
<span v-if="person.contact.github" id="github"><a :href="contactLinks.github"><i class="fa fa-code-fork" aria-hidden="true"></i> {{ person.contact.github_label || 'Git' }}</a></span>
<span v-if="person.contact.github_profile && contactLinks.github_profile" id="github-profile"><a :href="contactLinks.github_profile"><i class="fa fa-github" aria-hidden="true"></i> {{ person.contact.github_profile_label || ('@' + String(person.contact.github_profile).replace(/^@/, '')) }}</a></span>
<span v-if="person.contact.personal_site" id="personal-site"><a :href="contactLinks.personal_site"><i class="fa fa-globe" aria-hidden="true"></i> {{ person.contact.personal_site_label || person.contact.personal_site }}</a></span>
<span v-if="person.birth && person.birth.location" id="location"><i class="fa fa-map-marker" aria-hidden="true"></i> {{person.birth.location}}</span>
</div>
</div>
<div id="header-right">
<div id="headshot"></div>
</div>
</div>
<div id="resume-about" v-if="person.about">
<h2>{{ lang.about }}</h2>
<p>{{person.about}}</p>
<p v-if="person.core_strengths" class="core-strengths"><span class="core-strengths-label">Core strengths:</span> {{person.core_strengths}}</p>
</div>
<div id="resume-body">
<div id="experience-container">
<h2 id="experience-title">{{ lang.experience }}</h2>
<p
v-if="person.experience && person.experience.length"
class="experience-legend"
role="note">
<span class="experience-legend-intro">{{ lang.experienceLegendIntro }}:</span>
<span
v-for="item in workArrangementLegendItems()"
:key="item.key"
class="experience-legend-item">
<i :class="item.icon" aria-hidden="true"></i>
<span class="experience-legend-text">{{ item.label }}</span>
</span>
</p>
<div class="spacer"></div>
<div class="experience" v-for="experience in person.experience" :key="experience.company">
<h2 class="company">{{experience.company}}</h2>
<p class="job-info"><span class="job-title">{{experience.position}} | </span><span class="experience-timeperiod">{{experience.timeperiod}}</span></p>
<p class="job-description" v-if="experience.description">{{experience.description}}</p>
<h2 class="company-row">
<span class="company-primary">
<span class="company">{{experience.company}}</span><!--
--><span
v-if="experienceWorkBadges(experience).length"
class="exp-name-icon-gutter"
aria-hidden="true"></span><!--
--><span
v-if="experienceWorkBadges(experience).length"
class="experience-icons"
aria-hidden="true">
<span
v-for="b in experienceWorkBadges(experience)"
:key="b.key"
class="exp-icon-cell">
<i
:class="b.icon"
:title="b.label"></i>
</span>
</span>
</span>
<span v-if="experienceLocation(experience)" class="company-location">{{ experienceLocation(experience) }}</span>
</h2>
<p class="job-info" v-if="!experience.sub_companies">
<span class="job-title">{{experience.position}}</span>
<span class="experience-timeperiod">{{ experienceDateRange(experience) }}</span>
</p>
<p class="job-info sub-companies-line" v-if="experience.sub_companies && experience.sub_companies.length">
<span class="sub-companies-list">
<span
v-for="(sub, si) in experience.sub_companies"
:key="si"
class="sub-company-entry">
<span class="sub-company-name">{{ sub.name }}</span><!--
--><span
v-if="experienceWorkBadges(sub).length"
class="sub-icon-gutter"
aria-hidden="true"></span><!--
--><span
v-for="b in experienceWorkBadges(sub)"
:key="b.key"
class="sub-icon-cell"
aria-hidden="true"><i
:class="b.icon"
:title="b.label"></i></span>
<span
v-if="si < experience.sub_companies.length - 1"
class="sub-company-sep"
aria-hidden="true">&middot;</span>
</span>
</span>
<span class="experience-timeperiod">{{ experienceDateRange(experience) }}</span>
</p>
<ul
v-if="experienceDescriptionLines(experience).length"
class="job-description-list">
<li
v-for="(line, idx) in experienceDescriptionLines(experience)"
:key="idx"
class="list-item-black">{{ stripLeadingBullet(line) }}</li>
</ul>
<ul v-if="experience.list" >
<li v-for="(item, index) in experience.list" :key="index">
<span class="list-item-black">
@ -33,7 +112,22 @@
</ul>
</div>
</div>
<div id="education-container">
<div id="projects-container" v-if="person.projects && person.projects.length">
<h2 id="projects-title">{{ lang.projects }}</h2>
<div class="spacer"></div>
<div class="project" v-for="project in person.projects" :key="project.name">
<h2 class="project-name">{{ project.name }}</h2>
<ul
v-if="experienceDescriptionLines(project).length"
class="job-description-list">
<li
v-for="(line, idx) in experienceDescriptionLines(project)"
:key="idx"
class="list-item-black">{{ stripLeadingBullet(line) }}</li>
</ul>
</div>
</div>
<div id="education-container" v-if="person.education && person.education.length">
<h2 id="education-title">{{ lang.education }}</h2>
<div class="spacer"></div>
<div class="education" v-for="education in person.education" :key="education.degree">
@ -41,24 +135,19 @@
<p><span class="degree">{{education.degree}} | </span><span class="education-timeperiod">{{education.timeperiod}}</span></p>
</div>
</div>
<div id="skills-container" v-if="person.skills != []">
<div id="skills-container" v-if="person.skills && person.skills.length">
<h2 id="skills-title">{{ lang.skills }}</h2>
<div class="spacer"></div>
<p id="skill-description">{{person.knowledge}}</p>
<ul id="skill-list">
<li class="skill" v-for="skill in person.skills" :key="skill.name">
<span class="list-item-black">
{{skill.name}}
</span>
</li>
</ul>
<p
v-if="person.knowledge && String(person.knowledge).trim()"
id="skill-description">{{ person.knowledge }}</p>
<div id="skill-grid">
<div
v-for="skill in person.skills"
:key="skill.name"
class="skill-cell">{{ skill.name }}</div>
</div>
</div>
<div id="resume-footer">
<div v-if="person.about">
<h2>{{ lang.about }}</h2>
<p>{{person.about}}</p>
</div>
</div>
</div>
</template>
@ -68,15 +157,50 @@ import Vue from 'vue';
import { getVueOptions } from './options';
const name = 'green';
export default Vue.component(name, getVueOptions(name));
const baseOptions = getVueOptions(name);
export default Vue.component(name, Object.assign({}, baseOptions, {
methods: Object.assign({}, baseOptions.methods || {}, {
experienceLocation (exp) {
const loc = exp && exp.location;
return (loc !== undefined && loc !== null && String(loc).trim()) ? String(loc).trim() : '';
},
experienceDateRange (exp) {
const t = exp && exp.timeperiod;
return (t !== undefined && t !== null && String(t).trim()) ? String(t).trim() : '';
},
experienceDescriptionLines (exp) {
if (!exp || exp.description === undefined || exp.description === null) return [];
const raw = String(exp.description).trim();
if (!raw) return [];
let lines = raw.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
if (lines.length === 1 && lines[0].indexOf('•') !== -1) {
const parts = lines[0].split(/\s•\s/).map(p => p.trim()).filter(Boolean);
if (parts.length > 1) {
lines = parts.map((p) => (p.charAt(0) === '•' ? p : '• ' + p));
}
}
return lines;
},
stripLeadingBullet (line) {
return String(line).replace(/^[•*-]\s*/, '').trim();
}
})
}));
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style lang="less" scoped>
@text-green: #008000;
@pad-x: 26px;
#template {
box-sizing:border-box;
font-family:'Open Sans', sans-serif;
display: flex;
flex-direction: column;
min-height: 100%;
width: 100%;
max-width: 100%;
overflow-x: hidden;
h1, h2 {
/*font-family:'Open Sans Condensed', sans-serif;*/
margin:0;
@ -104,35 +228,35 @@ export default Vue.component(name, getVueOptions(name));
#resume-header {
color: white;
height: 136px;
background-color: green;
box-shadow: inset 0px 0px 200px #301030;
padding: 40px 100px 25px;
padding: 26px @pad-x 16px;
#header-left {
/*width: 465px;*/
width:100%;
float: left;
h1 {
font-size:56px;
font-size:46px;
color:white;
text-transform:uppercase;
line-height:56px;
line-height:46px;
}
h2 {
font-size:22px;
font-size:19px;
color:white;
}
#info-flex {
display:flex;
margin-top:20px;
font-size:14px;
flex-wrap: wrap;
margin-top:12px;
font-size:11.5px;
gap: 6px 0;
span {
margin-right:25px;
margin-right:20px;
}
i {
margin-right:5px;
margin-right:4px;
}
}
}
@ -155,79 +279,365 @@ export default Vue.component(name, getVueOptions(name));
}*/
}
#resume-body {
padding: 40px 100px;
#resume-about {
flex-shrink: 0;
padding: 8px @pad-x 10px;
background-color: green;
box-shadow: inset 0px 0px 100px #301030;
box-sizing: border-box;
h2 {
font-size: 16px;
text-transform: uppercase;
margin-bottom: 3px;
}
h2, p {
color: white;
}
p {
font-size: 10.5px;
line-height: 1.38;
margin: 0;
white-space: pre-line;
}
.core-strengths {
margin-top: 5px;
font-size: 10.5px;
line-height: 1.4;
white-space: normal;
opacity: 0.92;
}
.core-strengths-label {
font-weight: 700;
}
}
#experience-title, #education-title, #skills-title {
font-size:26px;
#resume-body {
flex: 1 1 auto;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
padding: 8px @pad-x 10px;
#experience-title, #education-title, #skills-title, #projects-title {
font-size:22px;
text-transform:uppercase;
}
.experience {
margin: 10px 0 10px 50px;
ul {
margin: 5px 0 0 0;
#experience-container {
padding-left: 12px;
padding-right: 12px;
box-sizing: border-box;
#experience-title {
margin-bottom: 5px;
}
}
.company, .education-description {
font-size:20px;
/* Block + inline-block: reliable in PDF print pipeline (avoids flex/padding quirks) */
.experience-legend {
display: block;
margin: 0 0 5px 0;
padding: 0;
font-size: 9px;
line-height: 1.45;
color: #5a7a5a;
max-width: 100%;
}
.experience-legend-intro {
display: inline-block;
font-weight: 700;
color: #4a6a4a;
margin-right: 12px;
vertical-align: baseline;
}
.experience-legend-item {
display: inline-block;
margin-right: 34px;
margin-bottom: 4px;
white-space: nowrap;
vertical-align: baseline;
&:last-child {
margin-right: 0;
}
i {
display: inline-block;
color: @text-green;
font-size: 11px;
width: 1.25em;
min-width: 1.25em;
margin-right: 7px;
text-align: center;
vertical-align: baseline;
}
}
.experience-legend-text {
display: inline;
font-weight: 400;
}
.experience {
margin: 0 0 8px 0;
break-inside: avoid;
page-break-inside: avoid;
&:last-child {
margin-bottom: 0;
}
ul {
margin: 4px 0 0 0;
}
}
.company-row {
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 4px 10px;
margin: 0 0 4px 0;
line-height: 1.2;
.company-primary {
display: inline;
vertical-align: baseline;
}
.company {
display: inline;
font-size: 15px;
font-weight: 700;
color: @text-green;
}
.exp-name-icon-gutter {
display: inline-block;
width: 10px;
min-width: 10px;
height: 1px;
vertical-align: baseline;
}
.experience-icons {
display: inline;
vertical-align: baseline;
}
.exp-icon-cell {
display: inline-block;
margin-right: 8px;
vertical-align: baseline;
&:last-child {
margin-right: 0;
}
i {
color: @text-green;
font-size: 13px;
line-height: 1;
opacity: 0.92;
vertical-align: baseline;
}
}
.company-location {
font-size: 10.5px;
font-weight: 400;
color: #5a7a5a;
margin-left: auto;
text-align: right;
max-width: 55%;
}
}
.education-description {
font-size: 18px;
line-height: 1.15;
}
.job-info {
margin-bottom:5px;
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 3px 10px;
margin: 0 0 4px 0;
line-height: 1.25;
}
.job-description-list {
margin: 0;
padding-left: 1.15em;
list-style-position: outside;
list-style-type: disc;
li {
font-size: 10.5px;
line-height: 1.32;
margin: 0 0 2px 0;
padding-left: 2px;
overflow-wrap: anywhere;
word-break: break-word;
}
li::marker {
color: @text-green;
}
}
.job-title, .degree {
font-weight:700;
color: @text-green;
font-size:16px;
font-size: 13px;
flex: 1 1 auto;
min-width: 40%;
}
.experience-timeperiod, .education-timeperiod {
.experience-timeperiod {
font-weight: 400;
color: #5a7a5a;
font-size: 10.5px;
line-height: 1.3;
margin-left: auto;
text-align: right;
max-width: 48%;
}
.sub-companies-line {
flex-wrap: wrap;
}
.sub-companies-list {
flex: 1 1 auto;
min-width: 40%;
}
.sub-company-entry {
display: inline;
white-space: nowrap;
}
.sub-company-name {
font-weight: 700;
color: @text-green;
font-size: 13px;
}
.sub-icon-gutter {
display: inline-block;
width: 8px;
}
.sub-icon-cell {
display: inline-block;
margin-right: 6px;
i {
color: @text-green;
font-size: 11px;
opacity: 0.92;
}
}
.sub-company-sep {
display: inline-block;
margin: 0 8px;
color: #5a7a5a;
font-weight: 700;
font-size: 13px;
}
.education-timeperiod {
font-weight:100;
color: @text-green;
font-size:16px;
font-size: 16px;
}
.education {
margin: 10px 0 10px 50px;
margin: 5px 0;
}
#skill-list {
column-count: 3;
list-style-position: inside;
ul li {
font-size:14px;
}
}
#education-container, #skills-container {
margin-top: 20px;
}
}
#resume-footer {
padding: 20px 100px;
height: 135px;
background-color: green;
box-shadow: inset 0px 0px 100px #301030;
#skills-container {
margin-top: 8px;
padding-left: 12px;
padding-right: 12px;
box-sizing: border-box;
position: absolute;
bottom: 0px;
width: 100%;
h2, p {
color:white;
#skills-title {
margin-bottom: 4px;
}
}
#skill-description {
font-size: 8.5px;
line-height: 1.28;
color: #444;
margin: 0 0 4px 0;
max-width: 100%;
box-sizing: border-box;
overflow-wrap: anywhere;
word-break: break-word;
column-count: 2;
column-gap: 12px;
column-fill: balance;
}
#skill-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
column-gap: 10px;
row-gap: 3px;
margin: 0;
padding: 0;
max-width: 100%;
box-sizing: border-box;
}
.skill-cell {
font-size: 9.5px;
line-height: 1.25;
color: #1a1a1a;
min-width: 0;
padding: 3px 6px;
background: #f6faf6;
border-left: 3px solid @text-green;
overflow-wrap: anywhere;
word-break: break-word;
break-inside: avoid;
}
#projects-container {
margin-top: 8px;
padding-left: 12px;
padding-right: 12px;
box-sizing: border-box;
#projects-title {
margin-bottom: 4px;
}
}
.project {
margin: 0 0 6px 0;
&:last-child {
margin-bottom: 0;
}
}
.project-name {
font-size: 15px;
font-weight: 700;
color: @text-green;
margin: 0 0 2px 0;
}
#education-container {
margin-top: 8px;
padding-left: 12px;
padding-right: 12px;
box-sizing: border-box;
#education-title {
margin-bottom: 4px;
}
}
}
}
.spacer {
width:100%;
border-bottom:1px solid @text-green;
margin:5px 0 10px;
margin: 3px 0 4px;
padding-top: 0;
box-sizing: border-box;
}
</style>

View File

@ -37,7 +37,7 @@
<td><i class="fa fa-home" aria-hidden="true"></i></td>
</tr>
<tr>
<td><a :href="person.contact.website">{{person.contact.website}}</a></td>
<td><a :href="contactLinks.website">{{person.contact.website}}</a></td>
<td><i class="fa fa-globe" aria-hidden="true"></i></td>
</tr>
<tr>
@ -48,7 +48,7 @@
</div>
</div>
<div class="right half">
<div class="education">
<div v-if="person.education && person.education.length" class="education">
<h3>{{ lang.education }}</h3>
<div class="education-block" v-for="education in person.education">
<span class="degree">{{education.degree}}</span>

View File

@ -38,7 +38,7 @@
</tr>
<tr v-if="person.contact.website">
<td><i class="fa fa-globe" aria-hidden="true"></i></td>
<td><a :href="person.contact.website">{{person.contact.website}}</a></td>
<td><a :href="contactLinks.website">{{person.contact.website}}</a></td>
</tr>
<tr v-if="person.contact.github">
<td><i class="fa fa-github" aria-hidden="true"></i></td>
@ -48,7 +48,7 @@
</div>
</div>
<div class="right half">
<div class="education">
<div v-if="person.education && person.education.length" class="education">
<h3>{{ lang.education }}</h3>
<div class="education-block" v-for="education in person.education" :key="education.degree">
<span class="degree">{{education.degree}}</span>

View File

@ -37,7 +37,7 @@
<td><i class="fa fa-home" aria-hidden="true"></i></td>
</tr>
<tr v-if="person.contact.website">
<td><a :href="person.contact.website">{{person.contact.website}}</a></td>
<td><a :href="contactLinks.website">{{person.contact.website}}</a></td>
<td><i class="fa fa-globe" aria-hidden="true"></i></td>
</tr>
<tr v-if="person.contact.github">
@ -48,7 +48,7 @@
</div>
</div>
<div class="right half">
<div class="education">
<div v-if="person.education && person.education.length" class="education">
<h3>{{ lang.education }}</h3>
<div class="education-block" v-for="education in person.education" :key="education.degree">
<span class="degree">{{education.degree}}</span>

View File

@ -64,7 +64,7 @@
</div>
</a>
<a :href="person.contact.website" target="_blank">
<a :href="contactLinks.website" target="_blank">
<div class="item">
<div class="icon">
<i class="material-icons">language</i>
@ -109,6 +109,7 @@
</div>
</div>
<template v-if="person.education && person.education.length">
<div class="section-headline">{{ lang.education }}</div>
<div class="block" v-for="education in person.education">
<div class="block-helper"></div>
@ -117,6 +118,7 @@
{{education.timeperiod}}, {{education.description}}
</p>
</div>
</template>
</div>
<div class="farRightCol">
<div class="section-headline">{{ lang.projects }}</div>

View File

@ -64,7 +64,7 @@
</div>
</a>
<a v-if="person.contact.website" :href="person.contact.website" target="_blank">
<a v-if="person.contact.website" :href="contactLinks.website" target="_blank">
<div class="item">
<div class="icon">
<i class="material-icons">language</i>
@ -122,6 +122,7 @@
</p>
</a>
</div>
<template v-if="person.education && person.education.length">
<div class="section-headline">{{ lang.education }}</div>
<div class="block" v-for="education in person.education" :key="education.degree">
<a
@ -133,6 +134,7 @@
</p>
</a>
</div>
</template>
</div>
<div style="clear:both;"></div>

View File

@ -46,7 +46,7 @@
</div>
</div>
<div class="education">
<div v-if="person.education && person.education.length" class="education">
<h3>{{ lang.education }}</h3>
<div class="education-block" v-for="education in person.education">
<div class="row">
@ -77,7 +77,7 @@
<span>;&nbsp;</span>
<span>{{person.contact.street}}, {{person.contact.city}}</span>
<span>;&nbsp;</span>
<a :href="person.contact.website">
<a :href="contactLinks.website">
{{person.contact.website}}</a>
<span>;&nbsp;</span>
<a :href="'https://github.com/'+person.contact.github">

View File

@ -30,7 +30,7 @@
</div>
</div>
</div>
<div class="education">
<div v-if="person.education && person.education.length" class="education">
<h3>{{ lang.education }}</h3>
<div class="education-block" v-for="education in person.education" :key="education.degree">
<div class="row">
@ -61,7 +61,7 @@
<span>;&nbsp;</span>
<span>{{person.contact.street}}, {{person.contact.city}}</span>
<span>;&nbsp;</span>
<a v-if="person.contact.website" :href="person.contact.website">
<a v-if="person.contact.website" :href="contactLinks.website">
{{person.contact.website}}</a>
<span v-if="person.contact.website">;&nbsp;</span>
<a v-if="person.contact.github" :href="'https://github.com/'+person.contact.github">

View File

@ -32,7 +32,7 @@
</div>
</div>
</div>
<div class="education">
<div v-if="person.education && person.education.length" class="education">
<h3>{{ lang.education }}</h3>
<div class="education-block" v-for="education in person.education" :key="education.degree">
<div class="row">
@ -63,7 +63,7 @@
<span>;&nbsp;</span>
<span>{{person.contact.street}}, {{person.contact.city}}</span>
<span>;&nbsp;</span>
<a v-if="person.contact.website" :href="person.contact.website">
<a v-if="person.contact.website" :href="contactLinks.website">
{{person.contact.website}}</a>
<span v-if="person.contact.website">;&nbsp;</span>
<a v-if="person.contact.github" :href="contactLinks.github">

View File

@ -6,6 +6,11 @@ import {
terms
} from '../terms';
function startsWithHttpScheme (value) {
const v = String(value).toLowerCase();
return v.startsWith('http://') || v.startsWith('https://');
}
// Called by templates to decrease redundancy
function getVueOptions (name) {
const opt = {
@ -35,8 +40,40 @@ function getVueOptions (name) {
contactLinks() {
const links = {};
if(this.person.contact.github) {
links.github = `https://github.com/${this.person.contact.github}`;
if (this.person.contact.github) {
const g = String(this.person.contact.github).trim();
if (startsWithHttpScheme(g)) {
links.github = g;
} else {
links.github = `https://github.com/${g}`;
}
}
if (this.person.contact.github_profile) {
const u = String(this.person.contact.github_profile)
.trim()
.replace(/^@/, '');
if (u) {
links.github_profile = `https://github.com/${u}`;
}
}
if (this.person.contact.website) {
const w = String(this.person.contact.website).trim();
if (startsWithHttpScheme(w)) {
links.website = w;
} else {
links.website = `https://${w}`;
}
}
if (this.person.contact.personal_site) {
const ps = String(this.person.contact.personal_site).trim();
if (startsWithHttpScheme(ps)) {
links.personal_site = ps;
} else {
links.personal_site = `https://${ps}`;
}
}
if(this.person.contact.codefights) {
@ -61,6 +98,100 @@ function getVueOptions (name) {
return links;
},
},
methods: {
experienceWorkBadges (exp) {
const badges = [];
if (!exp) return badges;
const r = exp.remote;
const rs = r !== undefined && r !== null && String(r).toLowerCase();
if (r === true || rs === 'true') {
badges.push({
key: 'remote',
icon: 'fa fa-home',
label: 'Remote'
});
} else if (rs === 'hybrid') {
badges.push({
key: 'hybrid',
icon: 'fa fa-exchange',
label: 'Hybrid'
});
} else if (r === false || rs === 'false') {
badges.push({
key: 'onsite',
icon: 'fa fa-building',
label: 'On-site'
});
}
const empRaw = exp.employment && String(exp.employment).toLowerCase();
const empTokens = empRaw ? empRaw.split(/[,;|]+/).map(t => t.trim().replace(/[\s_-]+/g, '')) : [];
const empSet = empTokens.length ? empTokens : [empRaw && empRaw.replace(/[\s_-]+/g, '')];
const empMap = {
contract: {
key: 'contract',
icon: 'fa fa-file-text-o',
label: 'Contract'
},
fulltime: {
key: 'fulltime',
icon: 'fa fa-briefcase',
label: 'Full-time'
},
coop: {
key: 'coop',
icon: 'fa fa-graduation-cap',
label: 'Co-op'
},
internship: {
key: 'coop',
icon: 'fa fa-graduation-cap',
label: 'Co-op'
}
};
const seen = {};
empSet.forEach(e => {
if (e && empMap[e] && !seen[empMap[e].key]) {
seen[empMap[e].key] = true;
badges.push(empMap[e]);
}
});
return badges;
},
workArrangementLegendItems () {
return [
{
key: 'remote',
icon: 'fa fa-home',
label: 'Remote'
},
{
key: 'hybrid',
icon: 'fa fa-exchange',
label: 'Hybrid'
},
{
key: 'onsite',
icon: 'fa fa-building',
label: 'On-site'
},
{
key: 'contract',
icon: 'fa fa-file-text-o',
label: 'Contract'
},
{
key: 'fulltime',
icon: 'fa fa-briefcase',
label: 'Full-time'
},
{
key: 'coop',
icon: 'fa fa-graduation-cap',
label: 'Co-op'
}
];
}
}
};
return opt;

View File

@ -8,8 +8,9 @@
<span id="email"><a :href='"mailto:" + person.contact.email'>
<i class="fa fa-envelope" aria-hidden="true"></i> {{person.contact.email}}</a></span>
<span id="phone"><i class='fa fa-phone-square' aria-hidden="true"></i> {{person.contact.phone}}</span>
<span v-if="person.contact.website" id="website"><a :href='person.contact.website'><i class="fa fa-home" aria-hidden="true"></i> {{person.contact.website}}</a></span>
<span v-if="person.contact.github" id="github"><a :href='contactLinks.github'><i class="fa fa-github" aria-hidden="true"></i> {{person.contact.github}}</a></span>
<span v-if="person.contact.website" id="website"><a :href="contactLinks.website"><i class="fa fa-linkedin" aria-hidden="true"></i> {{ person.contact.website_label || person.contact.website }}</a></span>
<span v-if="person.contact.github" id="github"><a :href="contactLinks.github"><i class="fa fa-code-fork" aria-hidden="true"></i> {{ person.contact.github_label || 'Git' }}</a></span>
<span v-if="person.contact.github_profile && contactLinks.github_profile" id="github-profile"><a :href="contactLinks.github_profile"><i class="fa fa-github" aria-hidden="true"></i> {{ person.contact.github_profile_label || ('@' + String(person.contact.github_profile).replace(/^@/, '')) }}</a></span>
</div>
</div>
<div id="header-right">
@ -19,9 +20,43 @@
<div id="resume-body">
<div id="experience-container">
<h2 id="experience-title">{{ lang.experience }}</h2>
<p
v-if="person.experience && person.experience.length"
class="experience-legend"
role="note">
<span class="experience-legend-intro">{{ lang.experienceLegendIntro }}:</span>
<span
v-for="item in workArrangementLegendItems()"
:key="item.key"
class="experience-legend-item">
<i :class="item.icon" aria-hidden="true"></i>
<span class="experience-legend-text">{{ item.label }}</span>
</span>
</p>
<div class="spacer"></div>
<div class="experience" v-for="experience in person.experience" :key="experience.company">
<h2 class="company">{{experience.company}}</h2>
<h2 class="company-row">
<span class="company-primary">
<span class="company">{{experience.company}}</span><!--
--><span
v-if="experienceWorkBadges(experience).length"
class="exp-name-icon-gutter"
aria-hidden="true"></span><!--
--><span
v-if="experienceWorkBadges(experience).length"
class="experience-icons"
aria-hidden="true">
<span
v-for="b in experienceWorkBadges(experience)"
:key="b.key"
class="exp-icon-cell">
<i
:class="b.icon"
:title="b.label"></i>
</span>
</span>
</span>
</h2>
<p class="job-info"><span class="job-title">{{experience.position}} | </span><span class="experience-timeperiod">{{experience.timeperiod}}</span></p>
<p class="job-description" v-if="experience.description">{{experience.description}}</p>
<ul v-if="experience.list" >
@ -33,7 +68,7 @@
</ul>
</div>
</div>
<div id="education-container">
<div id="education-container" v-if="person.education && person.education.length">
<h2 id="education-title">{{ lang.education }}</h2>
<div class="spacer"></div>
<div class="education" v-for="education in person.education" :key="education.degree">
@ -41,10 +76,12 @@
<p><span class="degree">{{education.degree}} | </span><span class="education-timeperiod">{{education.timeperiod}}</span></p>
</div>
</div>
<div id="skills-container" v-if="person.skills != []">
<div id="skills-container" v-if="person.skills && person.skills.length">
<h2 id="skills-title">{{ lang.skills }}</h2>
<div class="spacer"></div>
<p id="skill-description">{{person.knowledge}}</p>
<p
v-if="person.knowledge && String(person.knowledge).trim()"
id="skill-description">{{ person.knowledge }}</p>
<ul id="skill-list">
<li class="skill" v-for="skill in person.skills" :key="skill.name">
<span class="list-item-black">
@ -163,19 +200,120 @@ export default Vue.component(name, getVueOptions(name));
text-transform:uppercase;
}
.experience {
margin: 10px 0 10px 50px;
ul {
margin: 5px 0 0 0;
#experience-container {
padding-left: 50px;
box-sizing: border-box;
#experience-title {
margin-bottom: 16px;
}
}
.company, .education-description {
.experience-legend {
display: block;
margin: 0 0 14px 0;
padding: 0 12px 4px 0;
font-size: 10.5px;
line-height: 1.65;
color: #555;
max-width: 100%;
}
.experience-legend-intro {
display: inline-block;
font-weight: 700;
color: @text-purple;
margin-right: 12px;
vertical-align: baseline;
}
.experience-legend-item {
display: inline-block;
margin-right: 34px;
margin-bottom: 4px;
white-space: nowrap;
vertical-align: baseline;
&:last-child {
margin-right: 0;
}
i {
display: inline-block;
color: @text-purple;
font-size: 12px;
width: 1.25em;
min-width: 1.25em;
margin-right: 7px;
text-align: center;
vertical-align: baseline;
}
}
.experience-legend-text {
display: inline;
font-weight: 400;
color: #333;
}
.experience {
margin: 0 0 22px 0;
break-inside: avoid;
page-break-inside: avoid;
ul {
margin: 8px 0 0 0;
}
}
.company-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 10px 14px;
margin: 0 0 10px 0;
line-height: 1.28;
font-size: 20px;
.company-primary {
display: inline;
vertical-align: baseline;
}
.company {
display: inline;
font-size: 20px;
font-weight: 700;
color: @text-purple;
}
.exp-name-icon-gutter {
display: inline-block;
width: 24px;
min-width: 24px;
height: 1px;
vertical-align: baseline;
}
.experience-icons {
display: inline;
vertical-align: baseline;
}
.exp-icon-cell {
display: inline-block;
margin-right: 22px;
vertical-align: baseline;
&:last-child {
margin-right: 0;
}
i {
color: @text-purple;
font-size: 16px;
line-height: 1;
opacity: 0.9;
vertical-align: baseline;
}
}
}
.education-description {
font-size:20px;
}
.job-info {
margin-bottom:5px;
margin: 0 0 10px 0;
}
@ -227,7 +365,8 @@ export default Vue.component(name, getVueOptions(name));
.spacer {
width:100%;
border-bottom:1px solid @text-purple;
margin:5px 0 10px;
margin: 10px 0 10px;
padding-top: 4px;
box-sizing: border-box;
}
</style>

View File

@ -79,7 +79,7 @@
</div>
</div>
</div>
<div class="education">
<div v-if="person.education && person.education.length" class="education">
<h3>{{ lang.education }}</h3>
<div class="education-block" v-for="education in person.education">
<div class="row">

View File

@ -61,7 +61,7 @@
</div>
</div>
</div>
<div class="education">
<div v-if="person.education && person.education.length" class="education">
<h3>{{ lang.education }}</h3>
<div class="education-block" v-for="education in person.education">
<div class="row">

View File

@ -61,7 +61,7 @@
</div>
</div>
</div>
<div class="education">
<div v-if="person.education && person.education.length" class="education">
<h3>{{ lang.education }}</h3>
<div class="education-block" v-for="education in person.education" :key="education.degree">
<div class="row">

BIN
static/green.pdf Normal file

Binary file not shown.