/** * Web Resume & Technical Portfolio Controller * TEERASAK PROMSEN - Senior Software Engineer | Backend Engineer | System Engineer */ // Application State const state = { lang: localStorage.getItem('profile_lang') || 'th', theme: localStorage.getItem('profile_theme') || 'light', view: 'portfolio', // 'portfolio' | 'cv' skillFilter: 'all' }; // Initialize Application on DOM Ready document.addEventListener('DOMContentLoaded', () => { applyTheme(state.theme); setupEventListeners(); renderApp(); initVisitorSessionCounter(); }); /** * Visitor & Session Counter (No Database required) * Uses CounterAPI + sessionStorage to increment once per browser session */ async function initVisitorSessionCounter() { const badgeEl = document.getElementById('visitor-count-val'); if (!badgeEl) return; const isTh = state.lang === 'th'; const labelEl = document.getElementById('visitor-count-label'); if (labelEl) labelEl.textContent = isTh ? 'ผู้เข้าชม:' : 'Views:'; const namespace = 'teerasak-promsen-profile-v1'; const key = 'visits'; const hasVisitedInSession = sessionStorage.getItem('visited_session_flag'); try { const url = !hasVisitedInSession ? `https://api.counterapi.dev/v1/${namespace}/${key}/up` : `https://api.counterapi.dev/v1/${namespace}/${key}`; if (!hasVisitedInSession) { sessionStorage.setItem('visited_session_flag', 'true'); } const res = await fetch(url); if (!res.ok) throw new Error(`Status ${res.status}`); const data = await res.json(); if (data && typeof data.count !== 'undefined') { badgeEl.textContent = Number(data.count).toLocaleString(); } } catch (e) { badgeEl.textContent = '1+'; } } /** * Render the entire view based on current state */ function renderApp() { const data = PROFILE_DATA[state.lang]; if (!data) return; // Update HTML lang attribute document.documentElement.lang = state.lang; // Render Both Portfolio and CV Views renderHero(data.personal); renderSkills(data.skills); renderExperiences(data.experiences); renderExtraProjects(data.extraProjects); renderProjects(data.projects); renderArchitecture(data.architectureHighlights); renderEducation(data.education, data.certifications); renderCV(data); updateStaticTexts(); } /** * Set and apply visual theme */ function applyTheme(theme) { state.theme = theme; document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('profile_theme', theme); const themeIcon = document.getElementById('theme-icon'); if (themeIcon) { themeIcon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon'; } } /** * Switch Language (en / th) */ function setLanguage(lang) { if (lang !== 'en' && lang !== 'th') return; state.lang = lang; localStorage.setItem('profile_lang', lang); const langBtn = document.getElementById('lang-toggle-btn'); if (langBtn) { langBtn.textContent = lang === 'en' ? 'TH' : 'EN'; } renderApp(); } /** * Switch View Mode (Interactive Portfolio vs Print-Ready CV) */ function setViewMode(mode) { state.view = mode; const portfolioContainer = document.getElementById('portfolio-view-container'); const cvContainer = document.getElementById('cv-view-container'); const modePortfolioBtn = document.getElementById('mode-btn-portfolio'); const modeCvBtn = document.getElementById('mode-btn-cv'); if (mode === 'cv') { if (portfolioContainer) portfolioContainer.style.display = 'none'; if (cvContainer) cvContainer.style.display = 'block'; if (modePortfolioBtn) modePortfolioBtn.classList.remove('active'); if (modeCvBtn) modeCvBtn.classList.add('active'); window.scrollTo({ top: 0, behavior: 'smooth' }); } else { if (portfolioContainer) portfolioContainer.style.display = 'block'; if (cvContainer) cvContainer.style.display = 'none'; if (modePortfolioBtn) modePortfolioBtn.classList.add('active'); if (modeCvBtn) modeCvBtn.classList.remove('active'); } } /** * Render Header / Profile Overview */ function renderHero(personal) { const nameEl = document.getElementById('hero-name'); const titleEl = document.getElementById('hero-title'); const bioEl = document.getElementById('hero-bio'); const statusEl = document.getElementById('hero-status-text'); if (nameEl) nameEl.textContent = personal.name; if (titleEl) titleEl.textContent = personal.title; if (bioEl) bioEl.textContent = personal.bio; if (statusEl) statusEl.textContent = personal.status; } /** * Render Skills Matrix with Filter */ function renderSkills(skillsData) { const filterContainer = document.getElementById('skills-filter-container'); const gridContainer = document.getElementById('skills-grid-container'); if (filterContainer && skillsData.categories) { filterContainer.innerHTML = skillsData.categories.map(cat => ` `).join(''); } if (gridContainer && skillsData.items) { const filtered = state.skillFilter === 'all' ? skillsData.items : skillsData.items.filter(s => s.category === state.skillFilter); gridContainer.innerHTML = filtered.map(skill => `
${skill.name} ${skill.level}
`).join(''); } } function filterSkills(categoryId) { state.skillFilter = categoryId; renderSkills(PROFILE_DATA[state.lang].skills); } /** * Render Professional Experience (Official Employment) */ function renderExperiences(experiences) { const container = document.getElementById('experience-container'); if (!container || !experiences) return; container.innerHTML = experiences.map(exp => `

${exp.role}

${exp.period}
${exp.company} — [${exp.type}]

${exp.description}

${exp.tags.map(t => `${t}`).join('')}
`).join(''); } /** * Render Extra Projects & Independent Initiatives (AIDOSOL) */ function renderExtraProjects(extraProjects) { const container = document.getElementById('extra-projects-container'); if (!container || !extraProjects) return; container.innerHTML = extraProjects.map(proj => `

${proj.title}

${proj.period}
${proj.subtitle} — [${proj.type}]

${proj.description}

${proj.tags.map(t => `${t}`).join('')}
`).join(''); } /** * Render Key Production Projects */ function renderProjects(projects) { const gridContainer = document.getElementById('projects-grid-container'); if (!gridContainer || !projects) return; gridContainer.innerHTML = projects.map(proj => `
${proj.category}

${proj.title}

${proj.subtitle}

${proj.description}

${proj.tech.map(t => `${t}`).join('')}
`).join(''); } /** * Render System Architecture Highlights */ function renderArchitecture(archList) { const container = document.getElementById('arch-grid-container'); if (!container || !archList) return; container.innerHTML = archList.map(item => `

${item.title}

${item.desc}

`).join(''); } /** * Render Education and Certifications */ function renderEducation(education, certs) { const eduContainer = document.getElementById('education-container'); if (!eduContainer || !education) return; eduContainer.innerHTML = `

${education[0].degree}

${education[0].year}
${education[0].institution}

${education[0].details}

${state.lang === 'th' ? 'ใบรับรองและการฝึกอบรมทางเทคนิค' : 'Certifications & Technical Workshops'}
`; } /** * Render Executive Printable CV Mode */ function renderCV(data) { const cvSheet = document.getElementById('cv-sheet-content'); if (!cvSheet) return; const { personal, experiences, extraProjects, skills, education, projects, strengths, careerGoals } = data; const isTh = state.lang === 'th'; cvSheet.innerHTML = `

${personal.fullName}

${personal.title}
${personal.location}

${isTh ? 'บทสรุปประสบการณ์' : 'Executive Summary'}

${personal.bio}

${isTh ? 'ทักษะทางเทคนิค' : 'Core Technical Competencies'}

Backend Engineering: Node.js, TypeScript / JavaScript, NestJS, Express.js, Java & Spring Boot, Go, PHP, REST API, WebSocket, Microservices, Event-Driven, Saga Pattern, Idempotency, RBAC
CRM Loyalty & Gamification: ${isTh ? 'สถาปัตยกรรม Multi-Brand Loyalty, สมุดบัญชีแต้ม Real-Time (Earn/Burn Ledger), ระบบ Concurrency Lock ป้องกันการใช้แต้มซ้ำ (Anti-Double Spend), Dynamic Tier Progression, Gamification Engine (ภารกิจ Quests/Missions & เช็คอินต่อเนื่อง), วงล้อสุ่มของรางวัล (Lucky Wheel), ระบบแลกรับ Voucher ปริมาณสูง' : 'Multi-Brand Loyalty Architecture, Real-Time Point Ledgers (Earn/Burn), Anti-Double Spend Concurrency Locks, Dynamic Tier Progression, Gamification Engine (Quests, Missions & Streaks), Lucky Wheel Reward Distribution, High-Throughput Voucher Engines'}
Mobile Cross-Platform: ${isTh ? 'Flutter (Dart) — Hands-on พัฒนาแอปพลิเคชัน Rawmat & Jaiidee, React Native — Hands-on พัฒนา PAH Platform & ระบบ PVM (Petro Vending Management), Mobile State Management, IoT Telemetry & WebSocket Sync' : 'Flutter (Dart) — Hands-on on Rawmat & Jaiidee apps, React Native — Hands-on on PAH Platform & PVM (Petro Vending Management), Mobile State Management, IoT Telemetry & WebSocket Sync'}
Digital Signature, PKI & Document Integrity: ${isTh ? 'Electronic Signature (มาตรา 9), Trusted Signature (มาตรา 26), PKI & CA (มาตรา 28), Electronic Certificate, OTP, Cryptographic Hash (SHA-256) ตรวจสอบความสมบูรณ์และตรวจจับการแก้ไขเอกสาร, PDF Signing Workflow (PDF-lib), e-Stamp Duty (กรมสรรพากร)' : 'Electronic Signature (Sec. 9), Trusted Signature (Sec. 26), PKI & CA (Sec. 28), Electronic Certificate, OTP, Cryptographic Document Hashing (SHA-256) for Document Integrity & Tamper Detection, PDF Signing Workflow (PDF-lib), e-Stamp Duty (Revenue Dept.)'}
Database & Data: PostgreSQL, Oracle Database, MySQL / MariaDB, SQL Server, MongoDB, Redis, Sequelize, TypeORM, Database Transactions & Locks, CDC (Debezium / Kafka Concepts)
DevOps & Infrastructure: Docker, Docker Compose, Nginx, Linux / Ubuntu, GitLab CI/CD, Jenkins, SSL / TLS, Certbot, DNS, AWS (ECR, ECS, S3), Kubernetes Concepts
Observability & Troubleshooting: OpenTelemetry, Distributed Tracing (Jaeger), ELK Stack, Loki, Grafana, Node.js V8 Heap & GC Profiling, Production Root Cause Analysis
Frontend & Document: Next.js, React, Nuxt.js, Vue.js (Vue 2/3), Zustand, React Query, Responsive Web App, Server-side PDF Compilation & Manipulation

${isTh ? 'ประวัติการทำงานประจำ' : 'Professional Experience'}

${experiences.map(exp => `
${exp.role} ${exp.period}
${exp.company} — [${exp.type}]
`).join('')}
${extraProjects && extraProjects.length > 0 ? `

${isTh ? 'ผลงานเสริมและโครงการอิสระ' : 'Extra Projects & Independent Consulting'}

${extraProjects.map(proj => `
${proj.title} ${proj.period}
${proj.subtitle} — [${proj.type}]
`).join('')}
` : ''}

${isTh ? 'ประสบการณ์และโครงการสำคัญ' : 'Key Production Projects'}

${projects.slice(0, 6).map(proj => `
${proj.title} ${proj.tech.slice(0, 5).join(', ')}

${proj.description}

`).join('')}
${strengths && strengths.length > 0 ? `

${isTh ? 'จุดแข็งและความเชี่ยวชาญ' : 'Key Strengths'}

` : ''} ${careerGoals ? `

${isTh ? 'เป้าหมายทางสายอาชีพ' : 'Career Focus'}

${isTh ? 'เส้นทางเติบโต:' : 'Progression:'} ${careerGoals.progression}

${isTh ? 'หัวข้อที่กำลังพัฒนาต่อ:' : 'Focus Topics:'} ${careerGoals.topics.join(' • ')}
` : ''}

${isTh ? 'ประวัติการศึกษา' : 'Education'}

${education[0].degree} ${education[0].year}
${education[0].institution}
`; } /** * Update dynamic static UI texts */ function updateStaticTexts() { const isTh = state.lang === 'th'; const textMappings = { 'nav-about': isTh ? 'ภาพรวม' : 'Overview', 'nav-skills': isTh ? 'ทักษะ' : 'Skills', 'nav-experience': isTh ? 'ประวัติการทำงาน' : 'Experience', 'nav-extra-projects': isTh ? 'ผลงานเสริม' : 'Extra Projects', 'nav-projects': isTh ? 'โครงการสำคัญ' : 'Projects', 'nav-architecture': isTh ? 'สถาปัตยกรรม' : 'Architecture', 'sec-skills-title': isTh ? 'ทักษะทางเทคนิคและความเชี่ยวชาญ' : 'Technical Competencies', 'sec-skills-desc': isTh ? 'จำแนกความเชี่ยวชาญด้าน Backend, PKI & Digital Signature, ฐานข้อมูล, DevOps และ Observability' : 'Classified engineering proficiencies across backend development, digital trust, observability, and data infrastructure.', 'sec-exp-title': isTh ? 'ประวัติการทำงานประจำ' : 'Professional Experience', 'sec-exp-desc': isTh ? 'ประวัติการทำงานในองค์กรและระบบระดับ Enterprise ที่ส่งมอบสู่ Production' : 'Official corporate work history and mission-critical engineering deliveries.', 'sec-extra-title': isTh ? 'ผลงานเสริมและโครงการอิสระ' : 'Extra Projects & Independent Initiatives', 'sec-extra-desc': isTh ? 'โครงการวิจัยและพัฒนาอิสระ (AIDOSOL Solutions & Consulting) ซึ่งแยกจากประวัติการทำงานประจำ' : 'Independent R&D and specialized consulting projects separate from corporate employment.', 'sec-proj-title': isTh ? 'ประสบการณ์และโครงการสำคัญ' : 'Key Production Projects & Integrations', 'sec-proj-desc': isTh ? 'รายละเอียดเชิงเทคนิคของระบบที่ออกแบบและพัฒนาแบบ Zero to Production' : 'Technical breakdown of zero-to-production systems and enterprise architectures.', 'sec-arch-title': isTh ? 'จุดเด่นด้านสถาปัตยกรรมระบบ' : 'System Architecture Highlights', 'sec-arch-desc': isTh ? 'หลักการออกแบบ สถาปัตยกรรมความทนทาน และกระบวนการแก้ปัญหา Production' : 'Design principles, reliability patterns, and production engineering practices.', 'btn-download-cv': isTh ? 'บันทึกเป็น PDF' : 'Save as PDF', 'btn-back-portfolio': isTh ? 'กลับสู่หน้า Portfolio' : 'Back to Portfolio', 'btn-print-browser': isTh ? 'พิมพ์ผ่านเบราว์เซอร์' : 'Print (Browser)', 'btn-cv-pdf-label': isTh ? 'บันทึกเป็น PDF (pdf-lib)' : 'Save as PDF (pdf-lib)' }; for (const [id, val] of Object.entries(textMappings)) { const el = document.getElementById(id); if (el) el.textContent = val; } } /** * Setup Global Event Listeners */ function setupEventListeners() { // Theme Toggle Button const themeBtn = document.getElementById('theme-toggle-btn'); if (themeBtn) { themeBtn.addEventListener('click', () => { applyTheme(state.theme === 'dark' ? 'light' : 'dark'); }); } // Language Toggle Button const langBtn = document.getElementById('lang-toggle-btn'); if (langBtn) { langBtn.addEventListener('click', () => { setLanguage(state.lang === 'en' ? 'th' : 'en'); }); } // View Mode Switchers const modePortfolioBtn = document.getElementById('mode-btn-portfolio'); const modeCvBtn = document.getElementById('mode-btn-cv'); if (modePortfolioBtn) modePortfolioBtn.addEventListener('click', () => setViewMode('portfolio')); if (modeCvBtn) modeCvBtn.addEventListener('click', () => setViewMode('cv')); } /** * Print Trigger (Browser native print dialog) */ function printCV() { if (state.view !== 'cv') { setViewMode('cv'); setTimeout(() => { window.print(); }, 200); } else { window.print(); } } // In-Memory Font Cache for pdf-lib let pdfFontCache = { regular: null, bold: null }; /** * Dynamically ensure PDFLib and fontkit are available on window */ async function ensurePdfLibLoaded() { if (typeof window.PDFLib !== 'undefined' && typeof window.fontkit !== 'undefined') { return window.PDFLib; } const loadScript = (src) => new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = src; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); if (typeof window.fontkit === 'undefined') { try { await loadScript('libs/fontkit.umd.min.js'); } catch { await loadScript('https://unpkg.com/@pdf-lib/fontkit@1.1.1/dist/fontkit.umd.min.js').catch(console.warn); } } if (typeof window.PDFLib === 'undefined') { try { await loadScript('libs/pdf-lib.min.js'); } catch { await loadScript('https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js').catch(console.warn); } } return window.PDFLib; } /** * Generate and Download High-Quality A4 PDF using pdf-lib * Supports both Thai and English with perfect word segmentation and positioning */ async function generatePDFWithPdfLib() { const isTh = state.lang === 'th'; const data = PROFILE_DATA[state.lang]; if (!data) return; const btnNav = document.getElementById('btn-nav-pdf'); const btnCv = document.getElementById('btn-cv-export-pdf'); const originalNavHtml = btnNav ? btnNav.innerHTML : ''; const originalCvHtml = btnCv ? btnCv.innerHTML : ''; // Set loading state const loadingHtml = ` ${isTh ? 'กำลังสร้าง PDF...' : 'Generating PDF...'}`; if (btnNav) btnNav.innerHTML = loadingHtml; if (btnCv) btnCv.innerHTML = loadingHtml; try { const PDFLibModule = await ensurePdfLibLoaded(); if (!PDFLibModule || typeof PDFLibModule.PDFDocument === 'undefined') { throw new Error('PDFLib library could not be loaded. Please ensure scripts are accessible.'); } const { PDFDocument, rgb, StandardFonts } = PDFLibModule; const doc = await PDFDocument.create(); // Register FontKit if available if (typeof window.fontkit !== 'undefined') { doc.registerFontkit(window.fontkit); } // Load Unicode Fonts for Thai & English (Local file first, fallback to CDN) let fontRegular, fontBold; try { if (!pdfFontCache.regular || !pdfFontCache.bold) { const loadFontFile = async (localPath, cdnUrl) => { try { const r = await fetch(localPath); if (r.ok) return await r.arrayBuffer(); } catch {} const cdnRes = await fetch(cdnUrl); if (!cdnRes.ok) throw new Error(`Failed to load font from ${cdnUrl}`); return await cdnRes.arrayBuffer(); }; const [regBuffer, boldBuffer] = await Promise.all([ loadFontFile('fonts/Sarabun-Regular.ttf', 'https://raw.githubusercontent.com/google/fonts/main/ofl/sarabun/Sarabun-Regular.ttf'), loadFontFile('fonts/Sarabun-Bold.ttf', 'https://raw.githubusercontent.com/google/fonts/main/ofl/sarabun/Sarabun-Bold.ttf') ]); pdfFontCache.regular = regBuffer; pdfFontCache.bold = boldBuffer; } fontRegular = await doc.embedFont(pdfFontCache.regular); fontBold = await doc.embedFont(pdfFontCache.bold); } catch (fontErr) { console.warn('Custom font load failed, falling back to standard Helvetica:', fontErr); fontRegular = await doc.embedFont(StandardFonts.Helvetica); fontBold = await doc.embedFont(StandardFonts.HelveticaBold); } // Geometry & Layout Tokens (A4: 595.28 x 841.89 pt) const pageWidth = 595.28; const pageHeight = 841.89; const marginLeft = 38; const marginRight = 38; const marginTop = 38; const marginBottom = 38; const contentWidth = pageWidth - marginLeft - marginRight; // 519.28 pt // Pastel brown executive palette const colorDark = rgb(0.22, 0.18, 0.16); // #382E28 const colorAccent = rgb(0.61, 0.40, 0.27); // #9C6644 const colorMuted = rgb(0.42, 0.36, 0.32); // #6B5B52 const colorBody = rgb(0.31, 0.25, 0.22); // #4F4038 const colorLine = rgb(0.87, 0.84, 0.78); // #DFD5C6 const colorHeaderLine = rgb(0.22, 0.18, 0.16);// #382E28 let currentPage = doc.addPage([pageWidth, pageHeight]); let currentY = pageHeight - marginTop; // Helper: Split text into lines matching width function splitTextIntoLines(text, font, size, maxWidth) { if (!text) return []; const lines = []; let words = []; if (typeof Intl !== 'undefined' && Intl.Segmenter) { const segmenter = new Intl.Segmenter(isTh ? 'th' : 'en', { granularity: 'word' }); words = Array.from(segmenter.segment(text)).map(s => s.segment); } else { words = text.split(/(\s+)/); } let currentLine = ''; for (const word of words) { const testLine = currentLine + word; const width = font.widthOfTextAtSize(testLine, size); if (width <= maxWidth) { currentLine = testLine; } else { if (currentLine.trim().length > 0) { lines.push(currentLine.trim()); currentLine = word.trimStart(); } else { for (const char of word) { if (font.widthOfTextAtSize(currentLine + char, size) <= maxWidth) { currentLine += char; } else { lines.push(currentLine); currentLine = char; } } } } } if (currentLine.trim().length > 0) { lines.push(currentLine.trim()); } return lines; } // Helper: Ensure vertical space, creating a new page if needed function ensureSpace(neededHeight) { if (currentY - neededHeight < marginBottom) { currentPage = doc.addPage([pageWidth, pageHeight]); currentY = pageHeight - marginTop; // Running Top Header on subsequent pages currentPage.drawText(`${data.personal.fullName} — ${data.personal.title}`, { x: marginLeft, y: currentY, size: 7.5, font: fontRegular, color: colorMuted }); currentY -= 4; currentPage.drawLine({ start: { x: marginLeft, y: currentY }, end: { x: marginLeft + contentWidth, y: currentY }, thickness: 0.5, color: colorLine }); currentY -= 14; } } // Helper: Draw Section Title & Divider Line function drawSectionHeader(title) { ensureSpace(28); currentY -= 6; currentPage.drawText(title.toUpperCase(), { x: marginLeft, y: currentY, size: 9.5, font: fontBold, color: colorDark }); currentY -= 4; currentPage.drawLine({ start: { x: marginLeft, y: currentY }, end: { x: marginLeft + contentWidth, y: currentY }, thickness: 0.75, color: colorLine }); currentY -= 9; } // Helper: Draw Paragraph function drawParagraph(text, x, maxWidth, size, font, color, lineHeight) { const lines = splitTextIntoLines(text, font, size, maxWidth); for (const line of lines) { ensureSpace(lineHeight); currentPage.drawText(line, { x: x, y: currentY, size: size, font: font, color: color }); currentY -= lineHeight; } } // Helper: Draw Bullet Point function drawBulletItem(text, x, maxWidth, size, font, color, lineHeight) { const bulletIndent = 11; const lines = splitTextIntoLines(text, font, size, maxWidth - bulletIndent); if (lines.length === 0) return; ensureSpace(lineHeight * lines.length); // Draw bullet symbol currentPage.drawText('•', { x: x + 2, y: currentY, size: size + 1, font: fontBold, color: colorMuted }); for (let i = 0; i < lines.length; i++) { if (i > 0) ensureSpace(lineHeight); currentPage.drawText(lines[i], { x: x + bulletIndent, y: currentY, size: size, font: font, color: color }); currentY -= lineHeight; } } // ========================================== // 1. CV HEADER (Name, Title, Contacts) // ========================================== const p = data.personal; // Candidate Name currentPage.drawText(p.fullName, { x: marginLeft, y: currentY, size: 17, font: fontBold, color: colorDark }); // Contact List (Right-aligned / top right block) const contacts = [ p.email, p.phone, p.location, p.gitlab.replace('https://', ''), p.github.replace('https://', ''), p.linkedin.replace('https://www.', '') ].filter(Boolean); let contactY = currentY; const contactFontSize = 7.5; for (const c of contacts) { const textWidth = fontRegular.widthOfTextAtSize(c, contactFontSize); currentPage.drawText(c, { x: marginLeft + contentWidth - textWidth, y: contactY, size: contactFontSize, font: fontRegular, color: colorMuted }); contactY -= 9.5; } currentY -= 14; // Job Title currentPage.drawText(p.title, { x: marginLeft, y: currentY, size: 9.5, font: fontBold, color: colorAccent }); currentY -= 12; // Header divider line const headerBottomY = Math.min(currentY, contactY - 2); currentPage.drawLine({ start: { x: marginLeft, y: headerBottomY }, end: { x: marginLeft + contentWidth, y: headerBottomY }, thickness: 1.25, color: colorHeaderLine }); currentY = headerBottomY - 10; // ========================================== // 2. PROFESSIONAL SUMMARY // ========================================== drawSectionHeader(isTh ? 'บทสรุปประสบการณ์' : 'Executive Summary'); drawParagraph(p.bio, marginLeft, contentWidth, 8, fontRegular, colorBody, 11.5); currentY -= 4; // ========================================== // 3. CORE TECHNICAL COMPETENCIES // ========================================== drawSectionHeader(isTh ? 'ทักษะทางเทคนิคและความเชี่ยวชาญ' : 'Core Technical Competencies'); const competencies = [ { cat: 'Backend Engineering:', val: 'Node.js, TypeScript / JavaScript, NestJS, Express.js, Java & Spring Boot, Go, PHP, REST API, WebSocket, Microservices, Event-Driven, Saga Pattern, Idempotency, RBAC' }, { cat: 'CRM Loyalty & Gamification:', val: isTh ? 'สถาปัตยกรรม Multi-Brand Loyalty, สมุดบัญชีแต้ม Real-Time (Earn/Burn Ledger), Concurrency Lock (Anti-Double Spend), Dynamic Tier Progression, Gamification Engine (เควสต์ภารกิจ/วงล้อสุ่มของรางวัล), High-Concurrency Voucher Engine' : 'Multi-Brand Loyalty Architecture, Real-Time Point Ledgers (Earn/Burn), Anti-Double Spend Concurrency Locks, Dynamic Tier Progression, Gamification Quest & Lucky Wheel Engines, High-Throughput Voucher System' }, { cat: 'Mobile Cross-Platform:', val: isTh ? 'Flutter (Dart) — Hands-on แอป Rawmat & Jaiidee, React Native — Hands-on PAH Platform & ระบบ PVM (Petro Vending Management), Mobile State Management, IoT Telemetry & WebSocket Sync' : 'Flutter (Dart) — Hands-on on Rawmat & Jaiidee apps, React Native — Hands-on on PAH Platform & PVM (Petro Vending Management), Mobile State Management, IoT Telemetry & WebSocket Sync' }, { cat: 'Digital Signature & PKI:', val: isTh ? 'Electronic Signature (มาตรา 9), Trusted Signature (มาตรา 26), PKI & CA (มาตรา 28), Electronic Certificate, OTP, Cryptographic Hash (SHA-256) ตรวจสอบความสมบูรณ์และตรวจจับการแก้ไขเอกสาร, PDF-lib Signing, e-Stamp Duty (กรมสรรพากร)' : 'Electronic Signature (Sec. 9), Trusted Signature (Sec. 26), PKI & CA (Sec. 28), Electronic Certificate, OTP, Cryptographic Document Hashing (SHA-256) for Document Integrity & Tamper Detection, PDF Signing (PDF-lib), e-Stamp Duty (Revenue Dept.)' }, { cat: 'Databases & Data:', val: 'PostgreSQL, Oracle Database, MySQL / MariaDB, SQL Server, MongoDB, Redis, Sequelize, TypeORM, Database Transactions & Locks, CDC (Debezium / Kafka Concepts)' }, { cat: 'DevOps & Observability:', val: 'Docker, Docker Compose, Nginx, Linux / Ubuntu, GitLab CI/CD, Jenkins, SSL/TLS, OpenTelemetry, Jaeger Distributed Tracing, ELK Stack, Loki, Grafana, Node.js V8 Heap Profiling & RCA' }, { cat: 'Frontend & Document:', val: 'Next.js, React, Nuxt.js, Vue.js (Vue 2/3), Zustand, React Query, Responsive Web App, Server-side PDF Compilation & Form Filling' } ]; for (const comp of competencies) { ensureSpace(12); const catText = comp.cat + ' '; const catWidth = fontBold.widthOfTextAtSize(catText, 8); const fullText = catText + comp.val; const lines = splitTextIntoLines(fullText, fontRegular, 8, contentWidth); if (lines.length > 0) { // First line contains category in bold ensureSpace(11.5 * lines.length); // Draw category prefix in bold currentPage.drawText(catText, { x: marginLeft, y: currentY, size: 8, font: fontBold, color: colorDark }); // Draw the rest of first line const restOfFirstLine = lines[0].slice(catText.length); if (restOfFirstLine) { currentPage.drawText(restOfFirstLine, { x: marginLeft + catWidth, y: currentY, size: 8, font: fontRegular, color: colorBody }); } currentY -= 11.5; // Remaining wrapped lines for (let i = 1; i < lines.length; i++) { ensureSpace(11.5); currentPage.drawText(lines[i], { x: marginLeft, y: currentY, size: 8, font: fontRegular, color: colorBody }); currentY -= 11.5; } } currentY -= 2; } currentY -= 4; // ========================================== // 4. PROFESSIONAL EXPERIENCE // ========================================== drawSectionHeader(isTh ? 'ประวัติการทำงานประจำ' : 'Professional Experience'); for (const exp of data.experiences) { ensureSpace(32); // Role & Period currentPage.drawText(exp.role, { x: marginLeft, y: currentY, size: 9, font: fontBold, color: colorDark }); const periodWidth = fontRegular.widthOfTextAtSize(exp.period, 8); currentPage.drawText(exp.period, { x: marginLeft + contentWidth - periodWidth, y: currentY, size: 8, font: fontRegular, color: colorMuted }); currentY -= 11; // Company & Type currentPage.drawText(`${exp.company} — [${exp.type}]`, { x: marginLeft, y: currentY, size: 8.2, font: fontBold, color: colorAccent }); currentY -= 10.5; // Description if (exp.description) { drawParagraph(exp.description, marginLeft, contentWidth, 7.8, fontRegular, colorMuted, 10.5); currentY -= 2; } // Achievements for (const a of exp.achievements) { drawBulletItem(a, marginLeft, contentWidth, 7.8, fontRegular, colorBody, 10.5); currentY -= 1.5; } currentY -= 6; } // ========================================== // 5. EXTRA & FREELANCE PROJECTS // ========================================== if (data.extraProjects && data.extraProjects.length > 0) { drawSectionHeader(isTh ? 'ผลงานเสริมและโครงการฟรีแลนซ์' : 'Extra & Freelance Engineering Projects'); for (const proj of data.extraProjects) { ensureSpace(28); // Title & Period currentPage.drawText(proj.title, { x: marginLeft, y: currentY, size: 8.8, font: fontBold, color: colorDark }); const periodWidth = fontRegular.widthOfTextAtSize(proj.period, 7.8); currentPage.drawText(proj.period, { x: marginLeft + contentWidth - periodWidth, y: currentY, size: 7.8, font: fontRegular, color: colorMuted }); currentY -= 10.5; // Subtitle & Type currentPage.drawText(`${proj.subtitle} — [${proj.type}]`, { x: marginLeft, y: currentY, size: 8, font: fontBold, color: colorAccent }); currentY -= 10; // Description if (proj.description) { drawParagraph(proj.description, marginLeft, contentWidth, 7.6, fontRegular, colorMuted, 10); currentY -= 2; } // Achievements for (const a of proj.achievements) { drawBulletItem(a, marginLeft, contentWidth, 7.6, fontRegular, colorBody, 10); currentY -= 1; } // Tags if (proj.tags && proj.tags.length > 0) { ensureSpace(10); const tagStr = 'Tech: ' + proj.tags.join(' • '); drawParagraph(tagStr, marginLeft + 11, contentWidth - 11, 7.2, fontRegular, colorMuted, 9.5); } currentY -= 6; } } // ========================================== // 6. KEY FEATURED PROJECTS // ========================================== if (data.projects && data.projects.length > 0) { drawSectionHeader(isTh ? 'ประสบการณ์และโครงการสำคัญ' : 'Key Production Projects'); for (const proj of data.projects.slice(0, 5)) { ensureSpace(24); currentPage.drawText(proj.title, { x: marginLeft, y: currentY, size: 8.5, font: fontBold, color: colorDark }); const techStr = proj.tech.slice(0, 5).join(', '); const techWidth = fontRegular.widthOfTextAtSize(techStr, 7.2); currentPage.drawText(techStr, { x: marginLeft + contentWidth - techWidth, y: currentY, size: 7.2, font: fontRegular, color: colorMuted }); currentY -= 10; if (proj.description) { drawParagraph(proj.description, marginLeft, contentWidth, 7.6, fontRegular, colorMuted, 9.5); currentY -= 1.5; } for (const f of proj.features.slice(0, 2)) { drawBulletItem(f, marginLeft, contentWidth, 7.5, fontRegular, colorBody, 9.5); } currentY -= 4; } } // ========================================== // 7. KEY STRENGTHS // ========================================== if (data.strengths && data.strengths.length > 0) { drawSectionHeader(isTh ? 'จุดแข็งและความเชี่ยวชาญ' : 'Key Strengths'); for (const s of data.strengths) { drawBulletItem(s, marginLeft, contentWidth, 7.8, fontRegular, colorBody, 10.5); currentY -= 1.5; } currentY -= 4; } // ========================================== // 8. EDUCATION // ========================================== if (data.education && data.education.length > 0) { drawSectionHeader(isTh ? 'ประวัติการศึกษา' : 'Education'); const edu = data.education[0]; ensureSpace(25); currentPage.drawText(edu.degree, { x: marginLeft, y: currentY, size: 8.8, font: fontBold, color: colorDark }); const yearWidth = fontRegular.widthOfTextAtSize(edu.year, 8); currentPage.drawText(edu.year, { x: marginLeft + contentWidth - yearWidth, y: currentY, size: 8, font: fontRegular, color: colorMuted }); currentY -= 11; currentPage.drawText(edu.institution, { x: marginLeft, y: currentY, size: 8.2, font: fontBold, color: colorAccent }); currentY -= 10; if (edu.details) { drawParagraph(edu.details, marginLeft, contentWidth, 7.8, fontRegular, colorMuted, 10); } } // ========================================== // 9. FOOTER & PAGE NUMBERING ACROSS ALL PAGES // ========================================== const totalPages = doc.getPageCount(); const pages = doc.getPages(); for (let i = 0; i < totalPages; i++) { const page = pages[i]; const footerY = 22; // Bottom subtle rule page.drawLine({ start: { x: marginLeft, y: footerY + 10 }, end: { x: marginLeft + contentWidth, y: footerY + 10 }, thickness: 0.5, color: colorLine }); // Left Footer text page.drawText(`${data.personal.fullName} — Senior Software Engineer | Backend & System Architecture`, { x: marginLeft, y: footerY, size: 7.2, font: fontRegular, color: colorMuted }); // Right Footer Page number const pageText = `${isTh ? 'หน้า' : 'Page'} ${i + 1} / ${totalPages}`; const pageTextWidth = fontRegular.widthOfTextAtSize(pageText, 7.2); page.drawText(pageText, { x: marginLeft + contentWidth - pageTextWidth, y: footerY, size: 7.2, font: fontRegular, color: colorMuted }); } // Save and Trigger Download const pdfBytes = await doc.save(); const blob = new Blob([pdfBytes], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `CV_Teerasak_Promsen_${isTh ? 'TH' : 'EN'}.pdf`; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => { URL.revokeObjectURL(url); }, 10000); } catch (err) { console.error('Error generating PDF with pdf-lib:', err); alert(isTh ? `เกิดข้อผิดพลาดในการสร้าง PDF: ${err.message}\nจะเปิดหน้าพิมพ์เบราว์เซอร์แทน` : `Error generating PDF: ${err.message}\nOpening browser print dialog instead.` ); printCV(); } finally { if (btnNav) btnNav.innerHTML = originalNavHtml; if (btnCv) btnCv.innerHTML = originalCvHtml; } }