Spaces:
Running
Running
| // State management | |
| let cvData = { | |
| personal: { | |
| name: '', | |
| email: '', | |
| phone: '', | |
| website: '', | |
| linkedin: '', | |
| github: '', | |
| location: '', | |
| headline: '' | |
| }, | |
| summary: { | |
| title: 'RESUMEN PROFESIONAL', | |
| content: '' | |
| }, | |
| experience: [], | |
| education: [], | |
| projects: [], | |
| skills: [], | |
| languages: [] | |
| }; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadFromLocalStorage(); | |
| setupEventListeners(); | |
| updatePreview(); | |
| updateChecklist(); | |
| }); | |
| // Event Listeners | |
| function setupEventListeners() { | |
| // Personal info | |
| ['name', 'email', 'phone', 'website', 'linkedin', 'github', 'location', 'headline'].forEach(field => { | |
| const element = document.getElementById(`input-${field}`); | |
| if (element) { | |
| element.addEventListener('input', (e) => { | |
| cvData.personal[field] = e.target.value; | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| }); | |
| } | |
| }); | |
| // Summary | |
| const summaryTitle = document.getElementById('input-summary-title'); | |
| const summaryContent = document.getElementById('input-summary-content'); | |
| if (summaryTitle) { | |
| summaryTitle.addEventListener('input', (e) => { | |
| cvData.summary.title = e.target.value; | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| }); | |
| } | |
| if (summaryContent) { | |
| summaryContent.addEventListener('input', (e) => { | |
| cvData.summary.content = e.target.value; | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| }); | |
| } | |
| } | |
| // Add functions | |
| function addExperience() { | |
| const id = Date.now(); | |
| cvData.experience.push({ | |
| id, | |
| company: '', | |
| position: '', | |
| stack: '', | |
| location: '', | |
| startDate: '', | |
| endDate: '', | |
| highlights: [''] | |
| }); | |
| renderExperience(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| function addEducation() { | |
| const id = Date.now(); | |
| cvData.education.push({ | |
| id, | |
| institution: '', | |
| degree: '', | |
| location: '', | |
| startDate: '', | |
| endDate: '', | |
| highlights: [''] | |
| }); | |
| renderEducation(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| function addProject() { | |
| const id = Date.now(); | |
| cvData.projects.push({ | |
| id, | |
| name: '', | |
| date: '', | |
| description: '', | |
| location: '', | |
| startDate: '', | |
| endDate: '', | |
| highlights: [''] | |
| }); | |
| renderProjects(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| function addSkill() { | |
| const id = Date.now(); | |
| cvData.skills.push({ | |
| id, | |
| label: '', | |
| details: '' | |
| }); | |
| renderSkills(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| function addLanguage() { | |
| const id = Date.now(); | |
| cvData.languages.push({ | |
| id, | |
| label: '', | |
| details: '' | |
| }); | |
| renderLanguages(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| // Render functions | |
| function renderExperience() { | |
| const container = document.getElementById('experience-container'); | |
| container.innerHTML = cvData.experience.map((exp, index) => ` | |
| <div class="border border-gray-200 rounded-lg p-4 bg-gray-50"> | |
| <div class="flex justify-between items-start mb-3"> | |
| <h3 class="font-semibold text-gray-700">Experiencia ${index + 1}</h3> | |
| <button onclick="removeExperience(${exp.id})" class="text-red-500 hover:text-red-700"> | |
| <i data-lucide="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3"> | |
| <input type="text" placeholder="Empresa *" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${exp.company}" onchange="updateExperience(${exp.id}, 'company', this.value)"> | |
| <input type="text" placeholder="Cargo *" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${exp.position}" onchange="updateExperience(${exp.id}, 'position', this.value)"> | |
| <input type="text" placeholder="Stack tecnológico" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${exp.stack}" onchange="updateExperience(${exp.id}, 'stack', this.value)"> | |
| <input type="text" placeholder="Ubicación" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${exp.location}" onchange="updateExperience(${exp.id}, 'location', this.value)"> | |
| <input type="text" placeholder="Fecha inicio (MM/AAAA)" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${exp.startDate}" onchange="updateExperience(${exp.id}, 'startDate', this.value)"> | |
| <input type="text" placeholder="Fecha fin (MM/AAAA o Presente)" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${exp.endDate}" onchange="updateExperience(${exp.id}, 'endDate', this.value)"> | |
| </div> | |
| <div class="space-y-2"> | |
| <label class="text-sm font-medium text-gray-700">Logros (bullets)</label> | |
| ${exp.highlights.map((h, i) => ` | |
| <div class="flex gap-2"> | |
| <span class="text-gray-500 mt-2">•</span> | |
| <input type="text" class="flex-1 px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${h}" onchange="updateHighlight('experience', ${exp.id}, ${i}, this.value)" placeholder="Describe un logro cuantificable..."> | |
| <button onclick="removeHighlight('experience', ${exp.id}, ${i})" class="text-red-500 hover:text-red-700 px-2">×</button> | |
| </div> | |
| `).join('')} | |
| <button onclick="addHighlight('experience', ${exp.id})" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium mt-2"> | |
| + Añadir logro | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| function renderEducation() { | |
| const container = document.getElementById('education-container'); | |
| container.innerHTML = cvData.education.map((edu, index) => ` | |
| <div class="border border-gray-200 rounded-lg p-4 bg-gray-50"> | |
| <div class="flex justify-between items-start mb-3"> | |
| <h3 class="font-semibold text-gray-700">Educación ${index + 1}</h3> | |
| <button onclick="removeEducation(${edu.id})" class="text-red-500 hover:text-red-700"> | |
| <i data-lucide="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3"> | |
| <input type="text" placeholder="Institución *" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${edu.institution}" onchange="updateEducation(${edu.id}, 'institution', this.value)"> | |
| <input type="text" placeholder="Carrera/Título *" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${edu.degree}" onchange="updateEducation(${edu.id}, 'degree', this.value)"> | |
| <input type="text" placeholder="Ubicación" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${edu.location}" onchange="updateEducation(${edu.id}, 'location', this.value)"> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <input type="text" placeholder="Inicio" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${edu.startDate}" onchange="updateEducation(${edu.id}, 'startDate', this.value)"> | |
| <input type="text" placeholder="Fin" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${edu.endDate}" onchange="updateEducation(${edu.id}, 'endDate', this.value)"> | |
| </div> | |
| </div> | |
| <div class="space-y-2"> | |
| <label class="text-sm font-medium text-gray-700">Detalres relevantes</label> | |
| ${edu.highlights.map((h, i) => ` | |
| <div class="flex gap-2"> | |
| <span class="text-gray-500 mt-2">•</span> | |
| <input type="text" class="flex-1 px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${h}" onchange="updateHighlight('education', ${edu.id}, ${i}, this.value)"> | |
| <button onclick="removeHighlight('education', ${edu.id}, ${i})" class="text-red-500 hover:text-red-700 px-2">×</button> | |
| </div> | |
| `).join('')} | |
| <button onclick="addHighlight('education', ${edu.id})" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium mt-2"> | |
| + Añadir detalle | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| function renderProjects() { | |
| const container = document.getElementById('project-container'); | |
| container.innerHTML = cvData.projects.map((proj, index) => ` | |
| <div class="border border-gray-200 rounded-lg p-4 bg-gray-50"> | |
| <div class="flex justify-between items-start mb-3"> | |
| <h3 class="font-semibold text-gray-700">Proyecto/Certificación ${index + 1}</h3> | |
| <button onclick="removeProject(${proj.id})" class="text-red-500 hover:text-red-700"> | |
| <i data-lucide="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3"> | |
| <input type="text" placeholder="Nombre *" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${proj.name}" onchange="updateProject(${proj.id}, 'name', this.value)"> | |
| <input type="text" placeholder="Fecha" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${proj.date}" onchange="updateProject(${proj.id}, 'date', this.value)"> | |
| <input type="text" placeholder="Ubicación" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${proj.location}" onchange="updateProject(${proj.id}, 'location', this.value)"> | |
| <div class="grid grid-cols-2 gap-2"> | |
| <input type="text" placeholder="Inicio" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${proj.startDate}" onchange="updateProject(${proj.id}, 'startDate', this.value)"> | |
| <input type="text" placeholder="Fin" class="w-full px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${proj.endDate}" onchange="updateProject(${proj.id}, 'endDate', this.value)"> | |
| </div> | |
| </div> | |
| <textarea placeholder="Descripción breve..." class="w-full px-3 py-2 border border-gray-300 rounded text-sm mb-3" | |
| rows="2" onchange="updateProject(${proj.id}, 'description', this.value)">${proj.description}</textarea> | |
| <div class="space-y-2"> | |
| <label class="text-sm font-medium text-gray-700">Logros/Detalles</label> | |
| ${proj.highlights.map((h, i) => ` | |
| <div class="flex gap-2"> | |
| <span class="text-gray-500 mt-2">•</span> | |
| <input type="text" class="flex-1 px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${h}" onchange="updateHighlight('projects', ${proj.id}, ${i}, this.value)"> | |
| <button onclick="removeHighlight('projects', ${proj.id}, ${i})" class="text-red-500 hover:text-red-700 px-2">×</button> | |
| </div> | |
| `).join('')} | |
| <button onclick="addHighlight('projects', ${proj.id})" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium mt-2"> | |
| + Añadir logro | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| function renderSkills() { | |
| const container = document.getElementById('skills-container'); | |
| container.innerHTML = cvData.skills.map((skill, index) => ` | |
| <div class="flex items-center gap-2"> | |
| <input type="text" placeholder="Habilidad (ej: JavaScript)" class="flex-1 px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${skill.label}" onchange="updateSkill(${skill.id}, 'label', this.value)"> | |
| <input type="text" placeholder="Nivel (ej: Avanzado)" class="w-1/3 px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${skill.details}" onchange="updateSkill(${skill.id}, 'details', this.value)"> | |
| <button onclick="removeSkill(${skill.id})" class="text-red-500 hover:text-red-700 p-2"> | |
| <i data-lucide="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| function renderLanguages() { | |
| const container = document.getElementById('languages-container'); | |
| container.innerHTML = cvData.languages.map((lang, index) => ` | |
| <div class="flex items-center gap-2"> | |
| <input type="text" placeholder="Idioma (ej: Inglés)" class="flex-1 px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${lang.label}" onchange="updateLanguage(${lang.id}, 'label', this.value)"> | |
| <input type="text" placeholder="Nivel (ej: C1)" class="w-1/3 px-3 py-2 border border-gray-300 rounded text-sm" | |
| value="${lang.details}" onchange="updateLanguage(${lang.id}, 'details', this.value)"> | |
| <button onclick="removeLanguage(${lang.id})" class="text-red-500 hover:text-red-700 p-2"> | |
| <i data-lucide="x" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| // Update functions | |
| function updateExperience(id, field, value) { | |
| const exp = cvData.experience.find(e => e.id === id); | |
| if (exp) { | |
| exp[field] = value; | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| } | |
| function updateEducation(id, field, value) { | |
| const edu = cvData.education.find(e => e.id === id); | |
| if (edu) { | |
| edu[field] = value; | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| } | |
| function updateProject(id, field, value) { | |
| const proj = cvData.projects.find(p => p.id === id); | |
| if (proj) { | |
| proj[field] = value; | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| } | |
| function updateSkill(id, field, value) { | |
| const skill = cvData.skills.find(s => s.id === id); | |
| if (skill) { | |
| skill[field] = value; | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| } | |
| function updateLanguage(id, field, value) { | |
| const lang = cvData.languages.find(l => l.id === id); | |
| if (lang) { | |
| lang[field] = value; | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| } | |
| function updateHighlight(section, id, index, value) { | |
| const item = cvData[section].find(item => item.id === id); | |
| if (item && item.highlights) { | |
| item.highlights[index] = value; | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| } | |
| function addHighlight(section, id) { | |
| const item = cvData[section].find(item => item.id === id); | |
| if (item) { | |
| item.highlights.push(''); | |
| if (section === 'experience') renderExperience(); | |
| else if (section === 'education') renderEducation(); | |
| else if (section === 'projects') renderProjects(); | |
| } | |
| } | |
| function removeHighlight(section, id, index) { | |
| const item = cvData[section].find(item => item.id === id); | |
| if (item && item.highlights) { | |
| item.highlights.splice(index, 1); | |
| if (section === 'experience') renderExperience(); | |
| else if (section === 'education') renderEducation(); | |
| else if (section === 'projects') renderProjects(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| } | |
| // Remove functions | |
| function removeExperience(id) { | |
| cvData.experience = cvData.experience.filter(e => e.id !== id); | |
| renderExperience(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| function removeEducation(id) { | |
| cvData.education = cvData.education.filter(e => e.id !== id); | |
| renderEducation(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| function removeProject(id) { | |
| cvData.projects = cvData.projects.filter(p => p.id !== id); | |
| renderProjects(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| function removeSkill(id) { | |
| cvData.skills = cvData.skills.filter(s => s.id !== id); | |
| renderSkills(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| function removeLanguage(id) { | |
| cvData.languages = cvData.languages.filter(l => l.id !== id); | |
| renderLanguages(); | |
| updatePreview(); | |
| updateChecklist(); | |
| saveToLocalStorage(); | |
| } | |
| // Preview generation | |
| function updatePreview() { | |
| // Personal info | |
| document.getElementById('preview-name').textContent = cvData.personal.name || 'TU NOMBRE'; | |
| const contactParts = []; | |
| if (cvData.personal.location) contactParts.push(cvData.personal.location); | |
| if (cvData.personal.phone) contactParts.push(cvData.personal.phone); | |
| if (cvData.personal.email) contactParts.push(cvData.personal.email); | |
| if (cvData.personal.linkedin) contactParts.push(`linkedin.com/in/${cvData.personal.linkedin}`); | |
| if (cvData.personal.github) contactParts.push(`github.com/${cvData.personal.github}`); | |
| if (cvData.personal.website) contactParts.push(cvData.personal.website); | |
| document.getElementById('preview-contact').innerHTML = contactParts.join(' | '); | |
| // Summary | |
| const summarySection = document.getElementById('preview-summary-section'); | |
| if (cvData.summary.content) { | |
| summarySection.style.display = 'block'; | |
| document.getElementById('preview-summary-title').textContent = cvData.summary.title || 'RESUMEN PROFESIONAL'; | |
| document.getElementById('preview-summary-content').textContent = cvData.summary.content; | |
| } else { | |
| summarySection.style.display = 'none'; | |
| } | |
| // Experience | |
| const expSection = document.getElementById('preview-experience-section'); | |
| const expContainer = document.getElementById('preview-experience'); | |
| if (cvData.experience.length > 0 && cvData.experience.some(e => e.company || e.position)) { | |
| expSection.style.display = 'block'; | |
| expContainer.innerHTML = cvData.experience.map(exp => ` | |
| <div class="mb-4"> | |
| <div class="flex justify-between items-baseline mb-1"> | |
| <div> | |
| <span class="font-bold text-sm">${exp.company}</span> | |
| ${exp.stack ? `<span class="text-xs text-gray-600"> | ${exp.stack}</span>` : ''} | |
| </div> | |
| <span class="text-sm italic">${exp.startDate}${exp.endDate ? ` - ${exp.endDate}` : ''}</span> | |
| </div> | |
| <div class="text-sm italic mb-1">${exp.position}${exp.location ? ` | ${exp.location}` : ''}</div> | |
| ${exp.highlights.filter(h => h).length > 0 ? ` | |
| <ul class="text-sm list-disc ml-5 space-y-0.5"> | |
| ${exp.highlights.filter(h => h).map(h => `<li>${h}</li>`).join('')} | |
| </ul> | |
| ` : ''} | |
| </div> | |
| `).join(''); | |
| } else { | |
| expSection.style.display = 'none'; | |
| } | |
| // Education | |
| const eduSection = document.getElementById('preview-education-section'); | |
| const eduContainer = document.getElementById('preview-education'); | |
| if (cvData.education.length > 0 && cvData.education.some(e => e.institution || e.degree)) { | |
| eduSection.style.display = 'block'; | |
| eduContainer.innerHTML = cvData.education.map(edu => ` | |
| <div class="mb-3"> | |
| <div class="flex justify-between items-baseline"> | |
| <span class="font-bold text-sm">${edu.institution}</span> | |
| <span class="text-sm italic">${edu.startDate}${edu.endDate ? ` - ${edu.endDate}` : ''}</span> | |
| </div> | |
| <div class="text-sm italic">${edu.degree}${edu.location ? ` | ${edu.location}` : ''}</div> | |
| ${edu.highlights.filter(h => h).length > 0 ? ` | |
| <ul class="text-sm list-disc ml-5 mt-1"> | |
| ${edu.highlights.filter(h => h).map(h => `<li>${h}</li>`).join('')} | |
| </ul> | |
| ` : ''} | |
| </div> | |
| `).join(''); | |
| } else { | |
| eduSection.style.display = 'none'; | |
| } | |
| // Projects | |
| const projSection = document.getElementById('preview-project-section'); | |
| const projContainer = document.getElementById('preview-projects'); | |
| if (cvData.projects.length > 0 && cvData.projects.some(p => p.name)) { | |
| projSection.style.display = 'block'; | |
| projContainer.innerHTML = cvData.projects.map(proj => ` | |
| <div class="mb-3"> | |
| <div class="flex justify-between items-baseline"> | |
| <span class="font-bold text-sm">${proj.name}</span> | |
| <span class="text-sm italic">${proj.date || (proj.startDate + (proj.endDate ? ` - ${proj.endDate}` : ''))}</span> | |
| </div> | |
| ${proj.description ? `<div class="text-sm mb-1">${proj.description}</div>` : ''} | |
| ${proj.location ? `<div class="text-sm italic">${proj.location}</div>` : ''} | |
| ${proj.highlights.filter(h => h).length > 0 ? ` | |
| <ul class="text-sm list-disc ml-5 mt-1"> | |
| ${proj.highlights.filter(h => h).map(h => `<li>${h}</li>`).join('')} | |
| </ul> | |
| ` : ''} | |
| </div> | |
| `).join(''); | |
| } else { | |
| projSection.style.display = 'none'; | |
| } | |
| // Skills | |
| const skillSection = document.getElementById('preview-skills-section'); | |
| if (cvData.skills.length > 0 && cvData.skills.some(s => s.label)) { | |
| skillSection.style.display = 'block'; | |
| document.getElementById('preview-skills').innerHTML = cvData.skills | |
| .filter(s => s.label) | |
| .map(s => `<span class="mr-4"><strong>${s.label}</strong>${s.details ? ` (${s.details})` : ''}</span>`) | |
| .join(' • '); | |
| } else { | |
| skillSection.style.display = 'none'; | |
| } | |
| // Languages | |
| const langSection = document.getElementById('preview-languages-section'); | |
| if (cvData.languages.length > 0 && cvData.languages.some(l => l.label)) { | |
| langSection.style.display = 'block'; | |
| document.getElementById('preview-languages').innerHTML = cvData.languages | |
| .filter(l => l.label) | |
| .map(l => `<span class="mr-4"><strong>${l.label}</strong>${l.details ? ` (${l.details})` : ''}</span>`) | |
| .join(' • '); | |
| } else { | |
| langSection.style.display = 'none'; | |
| } | |
| } | |
| // Checklist and ATS compliance | |
| function updateChecklist() { | |
| let score = 0; | |
| let maxScore = 6; | |
| // Check personal | |
| const hasPersonal = cvData.personal.name && cvData.personal.email && cvData.personal.phone && cvData.personal.location; | |
| updateCheckStatus('check-personal', hasPersonal); | |
| if (hasPersonal) score++; | |
| // Check summary | |
| const hasSummary = cvData.summary.content.length > 50; | |
| updateCheckStatus('check-summary', hasSummary); | |
| if (hasSummary) score++; | |
| // Check experience | |
| const hasExperience = cvData.experience.length > 0 && cvData.experience.some(e => e.company && e.position); | |
| updateCheckStatus('check-experience', hasExperience); | |
| if (hasExperience) score++; | |
| // Check education | |
| const hasEducation = cvData.education.length > 0 && cvData.education.some(e => e.institution && e.degree); | |
| updateCheckStatus('check-education', hasEducation); | |
| if (hasEducation) score++; | |
| // Check skills | |
| const hasSkills = cvData.skills.length > 0 && cvData.skills.some(s => s.label); | |
| updateCheckStatus('check-skills', hasSkills); | |
| if (hasSkills) score++; | |
| // ATS specific check | |
| const atsIssues = checkATSIssues(); | |
| const atsGood = atsIssues.length === 0; | |
| updateCheckStatus('check-ats', atsGood, atsGood ? 'Optimización ATS' : `Problemas ATS: ${atsIssues.length}`); | |
| if (atsGood) score++; | |
| // Update progress | |
| const percentage = Math.round((score / maxScore) * 100); | |
| document.getElementById('progress-percent').textContent = `${percentage}%`; | |
| document.getElementById('progress-bar').style.width = `${percentage}%`; | |
| // Update ATS tips | |
| const tipsContainer = document.getElementById('ats-tips'); | |
| if (atsIssues.length > 0) { | |
| tipsContainer.innerHTML = atsIssues.slice(0, 3).map(issue => `<li>• ${issue}</li>`).join(''); | |
| } else { | |
| tipsContainer.innerHTML = '<li class="text-green-700">✓ Formato compatible con ATS</li>'; | |
| } | |
| } | |
| function updateCheckStatus(id, completed, customText = null) { | |
| const element = document.getElementById(id); | |
| if (!element) return; | |
| const icon = element.querySelector('.check-icon'); | |
| const text = element.querySelector('.check-text'); | |
| if (completed) { | |
| icon.classList.remove('text-gray-300'); | |
| icon.classList.add('text-green-500'); | |
| icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-check-circle w-5 h-5"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>'; | |
| element.classList.add('opacity-100'); | |
| element.classList.remove('opacity-60'); | |
| } else { | |
| icon.classList.add('text-gray-300'); | |
| icon.classList.remove('text-green-500'); | |
| icon.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle w-5 h-5"><circle cx="12" cy="12" r="10"/></svg>'; | |
| element.classList.remove('opacity-100'); | |
| element.classList.add('opacity-60'); | |
| } | |
| if (customText) { | |
| text.textContent = customText; | |
| } | |
| } | |
| function checkATSIssues() { | |
| const issues = []; | |
| // Check for photos (none in our app, but good to note) | |
| // Check for tables (we don't use them) | |
| // Check section headers | |
| const standardHeaders = ['EXPERIENCIA', 'EDUCACIÓN', 'HABILIDADES', 'RESUMEN']; | |
| // Check contact info format | |
| if (cvData.personal.email && !cvData.personal.email.includes('@')) { | |
| issues.push('Formato de email inválido'); | |
| } | |
| // Check for keywords in experience | |
| if (cvData.experience.length > 0) { | |
| const hasMetrics = cvData.experience.some(exp => | |
| exp.highlights.some(h => /\d+/.test(h)) | |
| ); | |
| if (!hasMetrics) { | |
| issues.push('Añade métricas cuantificables (%, números) a tu experiencia'); | |
| } | |
| } | |
| // Check summary length | |
| if (cvData.summary.content && cvData.summary.content.length < 50) { | |
| issues.push('El resumen es muy corto (mínimo 50 caracteres recomendados)'); | |
| } | |
| // Check for skills variety | |
| if (cvData.skills.length < 3) { | |
| issues.push('Añade más habilidades técnicas relevantes'); | |
| } | |
| return issues; | |
| } | |
| // View switching | |
| function switchView(view) { | |
| const editorView = document.getElementById('editor-view'); | |
| const previewView = document.getElementById('preview-view'); | |
| const btnEditor = document.getElementById('btn-editor'); | |
| const btnPreview = document.getElementById('btn-preview'); | |
| if (view === 'editor') { | |
| editorView.classList.remove('hidden'); | |
| previewView.classList.add('hidden'); | |
| btnEditor.classList.add('bg-indigo-600', 'text-white'); | |
| btnEditor.classList.remove('text-gray-600', 'hover:bg-gray-100'); | |
| btnPreview.classList.remove('bg-indigo-600', 'text-white'); | |
| btnPreview.classList.add('text-gray-600', 'hover:bg-gray-100'); | |
| } else { | |
| editorView.classList.add('hidden'); | |
| previewView.classList.remove('hidden'); | |
| btnPreview.classList.add('bg-indigo-600', 'text-white'); | |
| btnPreview.classList.remove('text-gray-600', 'hover:bg-gray-100'); | |
| btnEditor.classList.remove('bg-indigo-600', 'text-white'); | |
| btnEditor.classList.add('text-gray-600', 'hover:bg-gray-100'); | |
| updatePreview(); | |
| } | |
| } | |
| function toggleMobileMenu() { | |
| // Simple mobile menu toggle if needed | |
| const sidebar = document.querySelector('aside'); | |
| sidebar.classList.toggle('hidden'); | |
| } | |
| // Storage | |
| function saveToLocalStorage() { | |
| localStorage.setItem('cvData', JSON.stringify(cvData)); | |
| } | |
| function loadFromLocalStorage() { | |
| const saved = localStorage.getItem('cvData'); | |
| if (saved) { | |
| cvData = JSON.parse(saved); | |
| // Load personal info | |
| Object.keys(cvData.personal).forEach(key => { | |
| const el = document.getElementById(`input-${key}`); | |
| if (el) el.value = cvData.personal[key]; | |
| }); | |
| // Load summary | |
| const sumTitle = document.getElementById('input-summary-title'); | |
| const sumContent = document.getElementById('input-summary-content'); | |
| if (sumTitle) sumTitle.value = cvData.summary.title; | |
| if (sumContent) sumContent.value = cvData.summary.content; | |
| // Render all sections | |
| renderExperience(); | |
| renderEducation(); | |
| renderProjects(); | |
| renderSkills(); | |
| renderLanguages(); | |
| } | |
| } | |
| // Initialize checklist HTML | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const checklist = document.getElementById('checklist'); | |
| if (checklist) { | |
| checklist.innerHTML = ` | |
| <div id="check-personal" class="flex items-center space-x-3 p-2 rounded-lg transition"> | |
| <span class="check-icon text-gray-300"> | |
| <i data-lucide="circle" class="w-5 h-5"></i> | |
| </span> | |
| <span class="check-text text-sm font-medium">Información Personal</span> | |
| </div> | |
| <div id="check-summary" class="flex items-center space-x-3 p-2 rounded-lg transition"> | |
| <span class="check-icon text-gray-300"> | |
| <i data-lucide="circle" class="w-5 h-5"></i> | |
| </span> | |
| <span class="check-text text-sm font-medium">Resumen Profesional</span> | |
| </div> | |
| <div id="check-experience" class="flex items-center space-x-3 p-2 rounded-lg transition"> | |
| <span class="check-icon text-gray-300"> | |
| <i data-lucide="circle" class="w-5 h-5"></i> | |
| </span> | |
| <span class="check-text text-sm font-medium">Experiencia Laboral</span> | |
| </div> | |
| <div id="check-education" class="flex items-center space-x-3 p-2 rounded-lg transition"> | |
| <span class="check-icon text-gray-300"> | |
| <i data-lucide="circle" class="w-5 h-5"></i> | |
| </span> | |
| <span class="check-text text-sm font-medium">Educación</span> | |
| </div> | |
| <div id="check-skills" class="flex items-center space-x-3 p-2 rounded-lg transition"> | |
| <span class="check-icon text-gray-300"> | |
| <i data-lucide="circle" class="w-5 h-5"></i> | |
| </span> | |
| <span class="check-text text-sm font-medium">Habilidades</span> | |
| </div> | |
| <div id="check-ats" class="flex items-center space-x-3 p-2 rounded-lg transition"> | |
| <span class="check-icon text-gray-300"> | |
| <i data-lucide="circle" class="w-5 h-5"></i> | |
| </span> | |
| <span class="check-text text-sm font-medium">Optimización ATS</span> | |
| </div> | |
| `; | |
| lucide.createIcons(); | |
| } | |
| }); |