CV Thomas Jedelhauser

`; } /* ── State ── */ const state = { lang: 'en', darkMode: false, openJobs: new Set([1]), selectedIndustries: new Set(), selectedTags: new Set() }; const container = document.getElementById('thomas-cv'); const contentDiv = document.getElementById('cv-content'); const canvas = document.getElementById('cv-particles'); function getFilteredJobs() { return JOBS.filter(job => { if (state.selectedIndustries.size > 0) { const label = state.lang === 'de' ? job.industryDe : job.industry; if (!state.selectedIndustries.has(label)) return false; } if (state.selectedTags.size > 0 && !job.tags.some(tg => state.selectedTags.has(tg))) return false; return true; }); } function setLang(l) { state.lang = l; state.selectedIndustries.clear(); render(); } function toggleJob(id) { if (state.openJobs.has(id)) state.openJobs.delete(id); else state.openJobs.add(id); render(); } function toggleIndustry(ind) { if (state.selectedIndustries.has(ind)) state.selectedIndustries.delete(ind); else state.selectedIndustries.add(ind); render(); } function toggleTag(tag) { if (state.selectedTags.has(tag)) state.selectedTags.delete(tag); else state.selectedTags.add(tag); render(); } function setOpenAll(open) { const filtered = getFilteredJobs(); if (open) filtered.forEach(j => state.openJobs.add(j.id)); else filtered.forEach(j => state.openJobs.delete(j.id)); render(); } function clearFilters() { state.selectedIndustries.clear(); state.selectedTags.clear(); render(); } function toggleDark() { state.darkMode = !state.darkMode; render(); } /* ── Icons (inline SVG strings) ── */ const ICON_CHEVRON = ``; const ICON_DOWNLOAD = ``; const ICON_SUN = ``; const ICON_MOON = ``; /* ── Render ── */ function render() { const t = TRANSLATIONS[state.lang]; const industries = state.lang === 'de' ? INDUSTRIES_DE : INDUSTRIES_EN; const filtered = getFilteredJobs(); const hasFilters = state.selectedIndustries.size > 0 || state.selectedTags.size > 0; container.setAttribute('data-theme', state.darkMode ? 'dark' : 'light'); let html = ''; /* Controls */ html += `
`; t.downloads.forEach((dl, i) => { html += ``; }); html += `
`; /* Header */ html += `

${CONFIG.name}

${t.tagline}

${t.profile}

`; /* Education */ html += `

${t.education}

`; t.eduItems.forEach(e => { html += `${e.title} — ${e.institution}${e.year?`(${e.year})`:''}`; }); html += `
`; /* Filters */ html += `

${t.filterExperience}

${hasFilters?``:''}
${t.industry}
`; industries.forEach(ind => { html += ``; }); html += `
${t.expertise}
`; ALL_TAGS.forEach(tag => { html += ``; }); html += `
`; /* Timeline header */ html += `

${t.careerTimeline}

${filtered.length} ${t.of} ${JOBS.length} ${t.positions}
`; /* Jobs */ if (filtered.length === 0) { html += `
${t.noMatch}
`; } else { html += `
`; filtered.forEach(job => { const isOpen = state.openJobs.has(job.id); const isCurrent = !job.endDate; const loc = job[state.lang]; const industryLabel = state.lang === 'de' ? job.industryDe : job.industry; html += `
`; job.tags.forEach(tg => { html += `${tg}`; }); html += `
    `; loc.details.forEach(d => { html += `
  • ${d}
  • `; }); html += `
`; }); html += `
`; const allOpen = filtered.every(j => state.openJobs.has(j.id)); html += `
`; } contentDiv.innerHTML = html; resizeCanvas(); } /* ── Event delegation ── */ contentDiv.addEventListener('click', e => { const btn = e.target.closest('[data-action]'); if (!btn) return; const action = btn.dataset.action; if (action === 'lang-en') setLang('en'); else if (action === 'lang-de') setLang('de'); else if (action === 'toggle-dark') toggleDark(); else if (action === 'clear') clearFilters(); else if (action === 'expand') setOpenAll(true); else if (action === 'collapse') setOpenAll(false); else if (action === 'download') { const idx = parseInt(btn.dataset.index); const dl = TRANSLATIONS[state.lang].downloads[idx]; if (dl.type === 'generated') { const html = generatePdfHtml(state.lang); const win = window.open('', '_blank'); if (win) { win.document.write(html); win.document.close(); } } else if (dl.url) { window.open(dl.url, '_blank'); } } }); contentDiv.addEventListener('click', e => { const toggle = e.target.closest('.cv-job-toggle'); if (toggle) { const id = parseInt(toggle.closest('.cv-job').dataset.id); toggleJob(id); } }); contentDiv.addEventListener('click', e => { const tag = e.target.closest('.cv-filter-tag'); if (tag) { const type = tag.dataset.type; const val = tag.dataset.value; if (type === 'industry') toggleIndustry(val); else if (type === 'tag') toggleTag(val); } }); /* ── Particles ── */ let particlesInit = false; function resizeCanvas() { canvas.width = container.offsetWidth; canvas.height = container.scrollHeight; } function initParticles() { if (particlesInit) return; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; particlesInit = true; const ctx = canvas.getContext('2d'); let animId; const COUNT = 45; const CONNECT_DIST = 140; const MOUSE_DIST = 200; const particles = []; class Particle { constructor() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.vx = (Math.random() - 0.5) * 0.35; this.vy = (Math.random() - 0.5) * 0.35; this.r = Math.random() * 1.2 + 0.4; } update(mx, my) { this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > canvas.width) this.vx *= -1; if (this.y < 0 || this.y > canvas.height) this.vy *= -1; if (mx != null && my != null) { const dx = this.x - mx; const dy = this.y - my; const d = Math.sqrt(dx*dx + dy*dy); if (d < MOUSE_DIST) { const f = (MOUSE_DIST - d) / MOUSE_DIST; this.x += dx * f * 0.02; this.y += dy * f * 0.02; } } } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0, Math.PI*2); ctx.fillStyle = state.darkMode ? 'rgba(96,165,250,0.5)' : 'rgba(23,53,92,0.35)'; ctx.fill(); } } const mouse = { x: null, y: null }; function onMove(e) { const rect = container.getBoundingClientRect(); mouse.x = e.clientX - rect.left; mouse.y = e.clientY - rect.top + container.scrollTop; } function onLeave() { mouse.x = null; mouse.y = null; } resizeCanvas(); for (let i = 0; i < COUNT; i++) particles.push(new Particle()); function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(p => { p.update(mouse.x, mouse.y); p.draw(); }); for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const d = Math.sqrt(dx*dx + dy*dy); if (d < CONNECT_DIST) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); const alpha = (1 - d / CONNECT_DIST) * 0.25; const rgb = state.darkMode ? '96,165,250' : '23,53,92'; ctx.strokeStyle = `rgba(${rgb},${alpha})`; ctx.lineWidth = 0.8; ctx.stroke(); } } } animId = requestAnimationFrame(animate); } animate(); container.addEventListener('mousemove', onMove); container.addEventListener('mouseleave', onLeave); window.addEventListener('resize', resizeCanvas); } /* ── Boot ── */ function boot() { render(); initParticles(); } if (document.readyState !== 'loading') boot(); else document.addEventListener('DOMContentLoaded', boot); })();