383 lines
8.9 KiB
JavaScript
383 lines
8.9 KiB
JavaScript
const fs = require('fs');
|
||
const {
|
||
Document, Packer, Paragraph, TextRun, AlignmentType,
|
||
HeadingLevel, LevelFormat, ExternalHyperlink, BorderStyle
|
||
} = require('docx');
|
||
|
||
// ============================================
|
||
// CONFIGURATION - Modify these paths as needed
|
||
// ============================================
|
||
const INPUT_FILE = './resume.json';
|
||
const OUTPUT_FILE = `./Phillip_Tarrant_Resume_${new Date().getFullYear()}.docx`;
|
||
|
||
// Load resume data
|
||
const resumeData = JSON.parse(fs.readFileSync(INPUT_FILE, 'utf8'));
|
||
|
||
// ============================================
|
||
// HELPER FUNCTIONS
|
||
// ============================================
|
||
|
||
// Create a bullet point paragraph
|
||
function createBulletParagraph(text, reference) {
|
||
return new Paragraph({
|
||
numbering: { reference: reference, level: 0 },
|
||
spacing: { after: 60 },
|
||
children: [new TextRun({ text: text, size: 22, font: "Arial" })]
|
||
});
|
||
}
|
||
|
||
// Format date from YYYY-MM to "Mon YYYY"
|
||
function formatDate(dateStr) {
|
||
if (!dateStr) return '';
|
||
const date = new Date(dateStr + '-01');
|
||
return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
||
}
|
||
|
||
// ============================================
|
||
// BUILD DOCUMENT CONTENT
|
||
// ============================================
|
||
const children = [];
|
||
|
||
// --- HEADER SECTION ---
|
||
|
||
// Name - centered, bold, larger
|
||
children.push(new Paragraph({
|
||
alignment: AlignmentType.CENTER,
|
||
spacing: { after: 80 },
|
||
children: [new TextRun({
|
||
text: resumeData.basics.name,
|
||
bold: true,
|
||
size: 32, // 16pt
|
||
font: "Arial"
|
||
})]
|
||
}));
|
||
|
||
// Title/Label - centered
|
||
children.push(new Paragraph({
|
||
alignment: AlignmentType.CENTER,
|
||
spacing: { after: 80 },
|
||
children: [new TextRun({
|
||
text: resumeData.basics.label,
|
||
size: 24, // 12pt
|
||
font: "Arial"
|
||
})]
|
||
}));
|
||
|
||
// Contact line 1: email • phone
|
||
children.push(new Paragraph({
|
||
alignment: AlignmentType.CENTER,
|
||
spacing: { after: 40 },
|
||
children: [new TextRun({
|
||
text: `${resumeData.basics.email} • ${resumeData.basics.phone}`,
|
||
size: 22, // 11pt
|
||
font: "Arial"
|
||
})]
|
||
}));
|
||
|
||
// Contact line 2: LinkedIn • Location
|
||
const location = `${resumeData.basics.location.city}, ${resumeData.basics.location.region}`;
|
||
children.push(new Paragraph({
|
||
alignment: AlignmentType.CENTER,
|
||
spacing: { after: 200 },
|
||
children: [
|
||
new ExternalHyperlink({
|
||
link: resumeData.basics.url,
|
||
children: [new TextRun({
|
||
text: resumeData.basics.url.replace('https://', ''),
|
||
style: "Hyperlink",
|
||
size: 22,
|
||
font: "Arial"
|
||
})]
|
||
}),
|
||
new TextRun({
|
||
text: ` • ${location}`,
|
||
size: 22,
|
||
font: "Arial"
|
||
})
|
||
]
|
||
}));
|
||
|
||
// --- SUMMARY SECTION ---
|
||
children.push(new Paragraph({
|
||
spacing: { after: 240 },
|
||
children: [new TextRun({
|
||
text: resumeData.basics.summary,
|
||
size: 22,
|
||
font: "Arial"
|
||
})]
|
||
}));
|
||
|
||
// --- CAREER EXPERIENCE SECTION ---
|
||
children.push(new Paragraph({
|
||
spacing: { before: 120, after: 200 },
|
||
children: [new TextRun({
|
||
text: "Career Experience",
|
||
bold: true,
|
||
size: 26, // 13pt
|
||
font: "Arial"
|
||
})]
|
||
}));
|
||
|
||
// Work experience entries
|
||
resumeData.work.forEach((job, index) => {
|
||
const startDate = formatDate(job.startDate);
|
||
const endDate = job.endDate ? formatDate(job.endDate) : 'Present';
|
||
const dateRange = `${startDate} – ${endDate}`;
|
||
|
||
const companyLine = job.location ? `${job.name}, ${job.location}` : job.name;
|
||
|
||
// Job title and company
|
||
children.push(new Paragraph({
|
||
spacing: { before: 160, after: 60 },
|
||
children: [
|
||
new TextRun({
|
||
text: job.position,
|
||
bold: true,
|
||
size: 22,
|
||
font: "Arial"
|
||
}),
|
||
new TextRun({
|
||
text: `\n${companyLine}`,
|
||
size: 22,
|
||
font: "Arial"
|
||
}),
|
||
new TextRun({
|
||
text: ` ${dateRange}`,
|
||
size: 22,
|
||
font: "Arial",
|
||
italics: true
|
||
})
|
||
]
|
||
}));
|
||
|
||
// Job summary
|
||
if (job.summary) {
|
||
children.push(new Paragraph({
|
||
spacing: { after: 100 },
|
||
children: [new TextRun({
|
||
text: job.summary,
|
||
size: 22,
|
||
font: "Arial"
|
||
})]
|
||
}));
|
||
}
|
||
|
||
// Highlights as bullet points
|
||
if (job.highlights && job.highlights.length > 0) {
|
||
job.highlights.forEach(highlight => {
|
||
children.push(createBulletParagraph(highlight, "bullets"));
|
||
});
|
||
}
|
||
|
||
// Spacing after each job
|
||
children.push(new Paragraph({ spacing: { after: 120 }, children: [] }));
|
||
});
|
||
|
||
// --- EDUCATION, CERTIFICATIONS, AND AWARDS SECTION ---
|
||
children.push(new Paragraph({
|
||
spacing: { before: 200, after: 160 },
|
||
children: [new TextRun({
|
||
text: "Education, Certifications, and Awards",
|
||
bold: true,
|
||
size: 26,
|
||
font: "Arial"
|
||
})]
|
||
}));
|
||
|
||
// Certificates
|
||
if (resumeData.certificates) {
|
||
resumeData.certificates.forEach(cert => {
|
||
const year = cert.date ? new Date(cert.date).getFullYear() : '';
|
||
children.push(new Paragraph({
|
||
spacing: { after: 60 },
|
||
children: [
|
||
new TextRun({
|
||
text: cert.name,
|
||
bold: true,
|
||
size: 22,
|
||
font: "Arial"
|
||
}),
|
||
new TextRun({
|
||
text: `, ${cert.issuer}`,
|
||
size: 22,
|
||
font: "Arial"
|
||
}),
|
||
new TextRun({
|
||
text: year ? `, ${year}` : '',
|
||
size: 22,
|
||
font: "Arial"
|
||
})
|
||
]
|
||
}));
|
||
});
|
||
}
|
||
|
||
// Awards
|
||
if (resumeData.awards) {
|
||
resumeData.awards.forEach(award => {
|
||
const year = award.date ? new Date(award.date).getFullYear() : '';
|
||
children.push(new Paragraph({
|
||
spacing: { after: 60 },
|
||
children: [
|
||
new TextRun({
|
||
text: award.title,
|
||
bold: true,
|
||
size: 22,
|
||
font: "Arial"
|
||
}),
|
||
new TextRun({
|
||
text: `, ${award.awarder}`,
|
||
size: 22,
|
||
font: "Arial"
|
||
}),
|
||
new TextRun({
|
||
text: year ? `, ${year}` : '',
|
||
size: 22,
|
||
font: "Arial"
|
||
})
|
||
]
|
||
}));
|
||
if (award.summary) {
|
||
children.push(new Paragraph({
|
||
spacing: { after: 60 },
|
||
indent: { left: 360 },
|
||
children: [new TextRun({
|
||
text: award.summary,
|
||
size: 22,
|
||
font: "Arial",
|
||
italics: true
|
||
})]
|
||
}));
|
||
}
|
||
});
|
||
}
|
||
|
||
// Education
|
||
if (resumeData.education) {
|
||
resumeData.education.forEach(edu => {
|
||
const dateRange = edu.startDate && edu.endDate ? `${edu.startDate} – ${edu.endDate}` : '';
|
||
children.push(new Paragraph({
|
||
spacing: { after: 60 },
|
||
children: [
|
||
new TextRun({
|
||
text: `${edu.studyType} in ${edu.area}`,
|
||
bold: true,
|
||
size: 22,
|
||
font: "Arial"
|
||
}),
|
||
new TextRun({
|
||
text: `, ${edu.institution}`,
|
||
size: 22,
|
||
font: "Arial"
|
||
}),
|
||
new TextRun({
|
||
text: dateRange ? `, ${dateRange}` : '',
|
||
size: 22,
|
||
font: "Arial"
|
||
})
|
||
]
|
||
}));
|
||
if (edu.score) {
|
||
children.push(new Paragraph({
|
||
spacing: { after: 60 },
|
||
indent: { left: 360 },
|
||
children: [new TextRun({
|
||
text: `GPA: ${edu.score}`,
|
||
size: 22,
|
||
font: "Arial",
|
||
italics: true
|
||
})]
|
||
}));
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- TECHNICAL SKILLS SECTION ---
|
||
children.push(new Paragraph({
|
||
spacing: { before: 200, after: 160 },
|
||
children: [new TextRun({
|
||
text: "Technical Skills",
|
||
bold: true,
|
||
size: 26,
|
||
font: "Arial"
|
||
})]
|
||
}));
|
||
|
||
if (resumeData.skills) {
|
||
resumeData.skills.forEach(skillCategory => {
|
||
children.push(new Paragraph({
|
||
spacing: { after: 60 },
|
||
children: [
|
||
new TextRun({
|
||
text: `${skillCategory.name}: `,
|
||
bold: true,
|
||
size: 22,
|
||
font: "Arial"
|
||
}),
|
||
new TextRun({
|
||
text: skillCategory.keywords.join(", "),
|
||
size: 22,
|
||
font: "Arial"
|
||
})
|
||
]
|
||
}));
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// CREATE AND SAVE DOCUMENT
|
||
// ============================================
|
||
const doc = new Document({
|
||
styles: {
|
||
default: {
|
||
document: {
|
||
run: { font: "Arial", size: 22 }
|
||
}
|
||
}
|
||
},
|
||
numbering: {
|
||
config: [
|
||
{
|
||
reference: "bullets",
|
||
levels: [{
|
||
level: 0,
|
||
format: LevelFormat.BULLET,
|
||
text: "•",
|
||
alignment: AlignmentType.LEFT,
|
||
style: {
|
||
paragraph: {
|
||
indent: { left: 720, hanging: 360 }
|
||
}
|
||
}
|
||
}]
|
||
}
|
||
]
|
||
},
|
||
sections: [{
|
||
properties: {
|
||
page: {
|
||
size: {
|
||
width: 12240, // 8.5 inches (1440 DXA = 1 inch)
|
||
height: 15840 // 11 inches
|
||
},
|
||
margin: {
|
||
top: 720, // 0.5 inch
|
||
right: 1080, // 0.75 inch
|
||
bottom: 720, // 0.5 inch
|
||
left: 1080 // 0.75 inch
|
||
}
|
||
}
|
||
},
|
||
children: children
|
||
}]
|
||
});
|
||
|
||
// Save the document
|
||
Packer.toBuffer(doc).then(buffer => {
|
||
fs.writeFileSync(OUTPUT_FILE, buffer);
|
||
console.log(`Resume created successfully: ${OUTPUT_FILE}`);
|
||
}).catch(err => {
|
||
console.error('Error creating document:', err);
|
||
process.exit(1);
|
||
});
|