const fs = require('fs'); const { Document, Packer, Paragraph, TextRun, ExternalHyperlink, AlignmentType, LevelFormat, TabStopType, BorderStyle, HeadingLevel, } = require('docx'); // A4, 1 inch margins. Content width = 11906 - 2*1440 = 9026 DXA. const CONTENT_W = 9026; function styles() { return { default: { document: { run: { font: 'Arial', size: 21 } } }, // 10.5pt paragraphStyles: [ { id: 'Heading2', name: 'Heading 2', basedOn: 'Normal', next: 'Normal', quickFormat: true, run: { size: 24, bold: true, font: 'Arial', color: '1A1A1A' }, paragraph: { spacing: { before: 280, after: 120 }, outlineLevel: 1, border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: 'B0B0B0', space: 4 } }, } }, ], }; } const numbering = { config: [{ reference: 'b', levels: [{ level: 0, format: LevelFormat.BULLET, text: '•', alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 360, hanging: 220 } } } }], }], }; const bullet = (text) => new Paragraph({ numbering: { reference: 'b', level: 0 }, spacing: { after: 40 }, children: [new TextRun(text)] }); const heading = (text) => new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun(text)] }); // Role line: "Role — Org" left, dates right (tab stop). const roleLine = (left, dates) => new Paragraph({ spacing: { before: 120, after: 0 }, tabStops: [{ type: TabStopType.RIGHT, position: CONTENT_W }], children: [new TextRun({ text: left, bold: true }), new TextRun({ text: `\t${dates}`, color: '555555' })], }); const note = (text) => new Paragraph({ spacing: { after: 40 }, children: [new TextRun({ text, italics: true, color: '555555' })] }); const skillLine = (group, items) => new Paragraph({ spacing: { after: 40 }, children: [new TextRun({ text: `${group}: `, bold: true }), new TextRun(items)] }); const projectLine = (name, desc) => new Paragraph({ numbering: { reference: 'b', level: 0 }, spacing: { after: 40 }, children: [new TextRun({ text: `${name} — `, bold: true }), new TextRun(desc)] }); function buildCv(c) { const children = []; // Name + title children.push(new Paragraph({ spacing: { after: 0 }, children: [new TextRun({ text: c.name, bold: true, size: 40 })] })); children.push(new Paragraph({ spacing: { after: 60 }, children: [new TextRun({ text: c.title, size: 24, color: '333333' })] })); // Contact (with hyperlinks), rule beneath children.push(new Paragraph({ spacing: { after: 20 }, children: [ new TextRun(`${c.location} · `), new ExternalHyperlink({ link: `mailto:${c.email}`, children: [new TextRun({ text: c.email, style: 'Hyperlink' })] }), new TextRun(` · ${c.phone}`), ], })); children.push(new Paragraph({ spacing: { after: 40 }, border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: 'B0B0B0', space: 6 } }, children: [ new ExternalHyperlink({ link: 'https://cesnimda.co.uk', children: [new TextRun({ text: 'cesnimda.co.uk', style: 'Hyperlink' })] }), new TextRun(' · '), new ExternalHyperlink({ link: 'https://cesnimda.co.uk/Linkedin', children: [new TextRun({ text: 'LinkedIn', style: 'Hyperlink' })] }), ], })); // Summary children.push(heading(c.t.summary)); children.push(new Paragraph({ spacing: { after: 40 }, children: [new TextRun(c.summary)] })); // Skills children.push(heading(c.t.skills)); for (const s of c.skills) children.push(skillLine(s.group, s.items)); // Experience children.push(heading(c.t.experience)); for (const e of c.experience) { children.push(roleLine(`${e.role} — ${e.org}`, e.dates)); if (e.note) children.push(note(e.note)); for (const b of e.bullets) children.push(bullet(b)); } // Earlier roles children.push(new Paragraph({ spacing: { before: 100, after: 20 }, children: [new TextRun({ text: c.t.earlier, bold: true })] })); for (const r of c.earlier) children.push(bullet(r)); // Projects children.push(heading(c.t.projects)); for (const p of c.projects) children.push(projectLine(p.name, p.desc)); // Education children.push(heading(c.t.education)); children.push(roleLine(c.education.qual, c.education.dates)); children.push(new Paragraph({ children: [new TextRun(c.education.org)] })); return new Document({ styles: styles(), numbering, sections: [{ properties: { page: { size: { width: 11906, height: 16838 }, margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } }, children, }], }); } const EN = { name: 'Connor Babbington', title: 'Systems Developer', location: 'Tønsberg, Norway', email: 'connor.babbington@cesnimda.co.uk', phone: '+47 41 33 44 70', t: { summary: 'Professional Summary', skills: 'Core Skills', experience: 'Experience', earlier: 'Earlier roles (part-time, alongside the above)', projects: 'Projects', education: 'Education' }, summary: "Systems developer with eight years' experience building and maintaining production software for UK local government. A backend-leaning full-stack developer across C#, .NET, Python, JavaScript/TypeScript and SQL, with hands-on DevOps in Docker, Linux, CI/CD, Azure DevOps and GitHub. I turn stakeholder requirements into reliable, well-tested systems and support them in production. Based in Tønsberg with a valid residence permit. Native English speaker; Norwegian at B1 and actively developing. Open to remote, hybrid or on-site roles.", skills: [ { group: 'Development', items: 'C#, .NET, Python, JavaScript, TypeScript, React, SQL' }, { group: 'DevOps & Infrastructure', items: 'Docker, Linux, CI/CD, Azure DevOps, GitHub, nginx / reverse proxies, monitoring, self-hosting' }, { group: 'Practices', items: 'Testing, security hardening, OAuth2 integrations, production support, stakeholder communication' }, ], experience: [ { role: 'System Developer', org: 'Warwickshire County Council, UK', dates: '2015–2023', note: 'The first two years were completed as an apprenticeship.', bullets: [ 'Worked on the county-wide highways and streetlight fault-reporting system used across Warwickshire.', 'Designed, built and maintained full-stack applications in C#, Python, Ruby on Rails, SQL and JavaScript.', 'Delivered reliable, well-tested software and resolved stability and performance issues on live systems.', 'Owned deployments and production troubleshooting, and strengthened permissions and reliability.', 'Guided colleagues on best practices for code and process.', ] }, { role: 'Independent Development & Norwegian Study', org: 'Tønsberg, Norway', dates: '2023–Present', bullets: [ 'Building and self-hosting full-stack products (see Projects) while developing Norwegian language skills.', ] }, ], earlier: [ 'Sales Representative — Royal Vapes, UK (2017–2021)', 'Bartender — The Hodcarrier, UK (2016–2018)', 'Receptionist — Nuffield Health, UK (2014–2015)', ], projects: [ { name: 'JobTrack', desc: 'Full-stack job-application tracker (React, ASP.NET Core, SQLite, Docker) with a local AI summariser and Gmail integration.' }, { name: 'InboxIntel', desc: 'Gmail analytics and safe bulk-cleanup tool built as Clean Architecture in .NET 8 with PostgreSQL and encrypted OAuth tokens.' }, { name: 'Self-hosted infrastructure lab', desc: 'Ubuntu, Docker, nginx reverse proxy, authentication, monitoring and a self-hosted Gitea instance.' }, ], education: { qual: 'Extended Diploma — NVQ Level 3 in ICT', org: 'Warwickshire College, UK', dates: '2012–2015' }, }; const NO = { name: 'Connor Babbington', title: 'Systemutvikler', location: 'Tønsberg', email: 'connor.babbington@cesnimda.co.uk', phone: '+47 41 33 44 70', t: { summary: 'Sammendrag', skills: 'Kjernekompetanse', experience: 'Erfaring', earlier: 'Tidligere roller (deltid, ved siden av)', projects: 'Prosjekter', education: 'Utdanning' }, summary: 'Systemutvikler med åtte års erfaring med å bygge og vedlikeholde produksjonssystemer i britisk offentlig sektor. Fullstack-utvikler med tyngde på backend i C#, .NET, Python, JavaScript/TypeScript og SQL, med praktisk DevOps i Docker, Linux, CI/CD, Azure DevOps og GitHub. Jeg omsetter behov fra brukere og interessenter til pålitelige, godt testede systemer og drifter dem i produksjon. Bosatt i Tønsberg med gyldig oppholdstillatelse. Engelsk morsmål; norsk på B1-nivå og i aktiv utvikling. Åpen for remote, hybrid eller stedbaserte roller.', skills: [ { group: 'Utvikling', items: 'C#, .NET, Python, JavaScript, TypeScript, React, SQL' }, { group: 'DevOps og infrastruktur', items: 'Docker, Linux, CI/CD, Azure DevOps, GitHub, nginx / reverse proxy, overvåking, egendrift' }, { group: 'Arbeidsmåte', items: 'Testing, sikkerhet, OAuth2-integrasjoner, driftsstøtte, kommunikasjon med interessenter' }, ], experience: [ { role: 'Systemutvikler', org: 'Warwickshire County Council, UK', dates: '2015–2023', note: 'De to første årene var en lærlingperiode.', bullets: [ 'Jobbet på det fylkesdekkende systemet for feilmelding av veilys og veier, brukt i hele Warwickshire.', 'Designet, bygde og vedlikeholdt fullstack-applikasjoner i C#, Python, Ruby on Rails, SQL og JavaScript.', 'Leverte pålitelig, godt testet programvare og løste stabilitets- og ytelsesproblemer i produksjon.', 'Hadde ansvar for utrulling og feilsøking i drift, og styrket tilganger og pålitelighet.', 'Veiledet kollegaer i beste praksis for kode og prosess.', ] }, { role: 'Egen produktutvikling og norskkurs', org: 'Tønsberg', dates: '2023–nå', bullets: [ 'Bygger og drifter egne fullstack-produkter (se Prosjekter) samtidig som jeg utvikler norskkunnskapene.', ] }, ], earlier: [ 'Salgsrepresentant — Royal Vapes, UK (2017–2021)', 'Bartender — The Hodcarrier, UK (2016–2018)', 'Resepsjonist — Nuffield Health, UK (2014–2015)', ], projects: [ { name: 'JobTrack', desc: 'Fullstack jobbsøknads-tracker (React, ASP.NET Core, SQLite, Docker) med lokal AI-oppsummering og Gmail-integrasjon.' }, { name: 'InboxIntel', desc: 'Verktøy for Gmail-analyse og trygg masseopprydding, bygget som Clean Architecture i .NET 8 med PostgreSQL og krypterte OAuth-tokens.' }, { name: 'Egendriftet hjemmelab', desc: 'Ubuntu, Docker, nginx reverse proxy, autentisering, overvåking og egen Gitea-instans.' }, ], education: { qual: 'Extended Diploma — NVQ nivå 3 i IKT', org: 'Warwickshire College, UK', dates: '2012–2015' }, }; async function main() { await Packer.toBuffer(buildCv(EN)).then((b) => fs.writeFileSync('Connor-Babbington-CV-EN.docx', b)); await Packer.toBuffer(buildCv(NO)).then((b) => fs.writeFileSync('Connor-Babbington-CV-NO.docx', b)); console.log('wrote EN + NO docx'); } main();