<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Creador de Currículum Vitae</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
padding-bottom: 40px;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
padding: 30px 0;
background: linear-gradient(135deg, #2c3e50, #4a6491);
color: white;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.app-container {
display: flex;
flex-wrap: wrap;
gap: 30px;
}
.form-section {
flex: 1;
min-width: 300px;
background-color: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.preview-section {
flex: 1;
min-width: 300px;
background-color: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
h2 {
color: #2c3e50;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #4a6491;
display: flex;
align-items: center;
gap: 10px;
}
h2 i {
color: #4a6491;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
}
input, textarea {
width: 100%;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.3s;
}
input:focus, textarea:focus {
outline: none;
border-color: #4a6491;
}
textarea {
min-height: 100px;
resize: vertical;
}
.skills-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.skill-tag {
background-color: #eef2f7;
padding: 6px 12px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 5px;
font-size: 0.9rem;
}
.skill-tag .remove-skill {
cursor: pointer;
color: #e74c3c;
font-weight: bold;
}
.add-skill {
display: flex;
gap: 10px;
}
.add-skill input {
flex: 1;
}
.btn {
background-color: #4a6491;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: background-color 0.3s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:hover {
background-color: #3a5379;
}
.btn-primary {
background-color: #2c3e50;
}
.btn-primary:hover {
background-color: #1a252f;
}
.btn-secondary {
background-color: #3498db;
}
.btn-secondary:hover {
background-color: #2980b9;
}
.btn-danger {
background-color: #e74c3c;
}
.btn-danger:hover {
background-color: #c0392b;
}
.btn-success {
background-color: #27ae60;
}
.btn-success:hover {
background-color: #219653;
}
.btn-block {
display: block;
width: 100%;
margin-top: 20px;
padding: 12px;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.9rem;
}
/* Estilos para la foto */
.photo-upload-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
margin-bottom: 20px;
}
.photo-preview {
width: 150px;
height: 180px;
border-radius: 8px;
border: 3px solid #4a6491;
overflow: hidden;
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.photo-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.photo-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #7f8c8d;
text-align: center;
padding: 20px;
}
.photo-placeholder i {
font-size: 48px;
margin-bottom: 10px;
color: #bdc3c7;
}
.photo-upload-btn {
position: relative;
overflow: hidden;
}
.photo-upload-btn input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.photo-actions {
display: flex;
gap: 10px;
margin-top: 10px;
}
.photo-instructions {
font-size: 0.85rem;
color: #666;
text-align: center;
margin-top: 5px;
}
.cv-preview {
background-color: white;
min-height: 800px;
padding: 30px;
border: 1px solid #eee;
border-radius: 8px;
font-size: 0.95rem;
line-height: 1.5;
}
/* Encabezado SIN foto (versión anterior) */
.cv-header-no-photo {
background-color: #2c3e50;
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 30px;
}
/* Encabezado CON foto (nueva versión) */
.cv-header-with-photo {
background-color: #2c3e50;
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: flex-start;
position: relative;
min-height: 180px;
}
.cv-header-content {
flex: 1;
}
.cv-photo-container {
width: 140px;
height: 160px;
border-radius: 8px;
overflow: hidden;
border: 3px solid rgba(255, 255, 255, 0.3);
background-color: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
margin-left: 20px;
flex-shrink: 0;
}
.cv-photo {
width: 100%;
height: 100%;
object-fit: cover;
}
.cv-photo-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.7);
text-align: center;
padding: 20px;
font-size: 0.9rem;
}
.cv-photo-placeholder i {
font-size: 36px;
margin-bottom: 8px;
}
.cv-name {
font-size: 2.2rem;
margin-bottom: 5px;
}
.cv-title {
font-size: 1.4rem;
opacity: 0.9;
margin-bottom: 15px;
}
.cv-contact {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 15px;
}
.cv-contact-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.95rem;
}
.cv-section {
margin-bottom: 25px;
}
.cv-section-title {
color: #2c3e50;
border-bottom: 2px solid #4a6491;
padding-bottom: 5px;
margin-bottom: 15px;
font-size: 1.3rem;
}
.cv-item {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.cv-item:last-child {
border-bottom: none;
}
.cv-item-title {
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 1.1rem;
}
.cv-item-subtitle {
color: #4a6491;
font-style: italic;
margin-bottom: 5px;
}
.cv-item-dates {
color: #666;
font-size: 0.9rem;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 5px;
}
.current-badge {
background-color: #27ae60;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 600;
}
.cv-skills {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cv-skill {
background-color: #eef2f7;
padding: 6px 12px;
border-radius: 4px;
font-size: 0.9rem;
}
.actions {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 30px;
flex-wrap: wrap;
}
.instructions {
background-color: #eef2f7;
padding: 20px;
border-radius: 8px;
margin-top: 30px;
font-size: 0.95rem;
}
.instructions h3 {
margin-bottom: 10px;
color: #2c3e50;
}
.instructions ul {
padding-left: 20px;
}
.instructions li {
margin-bottom: 8px;
}
/* Estilos para la experiencia laboral dinámica */
.job-entry {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
border-left: 4px solid #4a6491;
}
.job-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.job-title {
font-weight: 600;
color: #2c3e50;
}
.current-job-toggle {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 15px;
}
.toggle-label {
font-size: 0.9rem;
color: #666;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #27ae60;
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.remove-job {
background-color: transparent;
border: none;
color: #e74c3c;
cursor: pointer;
font-size: 1.2rem;
padding: 5px;
}
.add-job-btn {
margin-top: 10px;
}
/* Estilos para el pie de página del PDF */
.pdf-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 10pt;
color: #666;
padding: 10px;
background-color: white;
border-top: 1px solid #eee;
}
.page-number {
font-family: Arial, sans-serif;
}
@media (max-width: 768px) {
.app-container {
flex-direction: column;
}
.cv-preview {
min-height: auto;
}
.actions {
flex-direction: column;
}
.job-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.cv-header-with-photo {
flex-direction: column;
align-items: center;
text-align: center;
min-height: auto;
}
.cv-photo-container {
margin-left: 0;
margin-bottom: 20px;
order: -1;
}
.cv-header-content {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-file-alt"></i> Creador de Currículum Vitae</h1>
<p class="subtitle">Completa el formulario y genera tu CV profesional en minutos</p>
</header>
<div class="app-container">
<!-- Sección del formulario -->
<section class="form-section">
<h2><i class="fas fa-edit"></i> Información Personal</h2>
<!-- Subida de foto -->
<div class="form-group">
<label>Foto de Perfil (Opcional)</label>
<div class="photo-upload-container">
<div class="photo-preview" id="photo-preview">
<div class="photo-placeholder" id="photo-placeholder">
<i class="fas fa-user-circle"></i>
<span>Sin foto</span>
</div>
<img id="photo-preview-img" style="display: none;">
</div>
<div class="photo-actions">
<button class="btn btn-secondary photo-upload-btn" id="upload-photo-btn">
<i class="fas fa-upload"></i> Subir Foto
<input type="file" id="photo-input" accept="image/*" style="display: none;">
</button>
<button class="btn btn-danger" id="remove-photo-btn" style="display: none;">
<i class="fas fa-trash"></i> Eliminar
</button>
</div>
<div class="photo-instructions">
<p><strong>Opcional:</strong> JPG o PNG, máximo 2MB</p>
<p>Con foto: CV moderno | Sin foto: CV clásico</p>
</div>
</div>
</div>
<div class="form-group">
<label for="name">Nombre Completo</label>
<input type="text" id="name" placeholder="Ej: Juan Pérez González">
</div>
<div class="form-group">
<label for="title">Título Profesional</label>
<input type="text" id="title" placeholder="Ej: Desarrollador Web Frontend">
</div>
<div class="form-group">
<label for="email">Correo Electrónico</label>
<input type="email" id="email" placeholder="Ej: juan.perez@email.com">
</div>
<div class="form-group">
<label for="phone">Teléfono</label>
<input type="text" id="phone" placeholder="Ej: +34 612 345 678">
</div>
<div class="form-group">
<label for="location">Ubicación</label>
<input type="text" id="location" placeholder="Ej: Madrid, España">
</div>
<div class="form-group">
<label for="summary">Resumen Profesional</label>
<textarea id="summary" placeholder="Breve descripción de tu experiencia, habilidades y objetivos profesionales..."></textarea>
</div>
<h2><i class="fas fa-briefcase"></i> Experiencia Laboral</h2>
<div id="jobs-container">
<!-- Las experiencias laborales se agregarán aquí dinámicamente -->
</div>
<div class="form-group">
<button class="btn btn-secondary add-job-btn" id="add-job-btn">
<i class="fas fa-plus"></i> Agregar Experiencia Laboral
</button>
</div>
<h2><i class="fas fa-graduation-cap"></i> Educación</h2>
<div class="form-group">
<label for="education1-degree">Título Académico</label>
<input type="text" id="education1-degree" placeholder="Ej: Grado en Ingeniería Informática">
</div>
<div class="form-group">
<label for="education1-school">Institución</label>
<input type="text" id="education1-school" placeholder="Ej: Universidad Complutense de Madrid">
</div>
<div class="form-group">
<label for="education1-dates">Fechas</label>
<input type="text" id="education1-dates" placeholder="Ej: 2016 - 2020">
</div>
<h2><i class="fas fa-star"></i> Habilidades</h2>
<div class="form-group">
<div class="skills-container" id="skills-container">
<!-- Las habilidades se agregarán aquí dinámicamente -->
</div>
<div class="add-skill">
<input type="text" id="new-skill" placeholder="Ej: JavaScript, React, HTML/CSS">
<button class="btn btn-sm" id="add-skill-btn">Agregar</button>
</div>
</div>
<button class="btn btn-primary btn-block" id="generate-pdf">
<i class="fas fa-download"></i> Descargar CV en PDF
</button>
</section>
<!-- Sección de vista previa -->
<section class="preview-section">
<h2><i class="fas fa-eye"></i> Vista Previa del CV</h2>
<div class="cv-preview" id="cv-preview">
<!-- El encabezado se actualizará dinámicamente según si hay foto o no -->
<div id="cv-header-container">
<!-- Se llenará dinámicamente con JavaScript -->
</div>
<div class="cv-section">
<div class="cv-section-title">Resumen Profesional</div>
<div id="cv-summary">Breve descripción de tu experiencia, habilidades y objetivos profesionales.</div>
</div>
<div class="cv-section">
<div class="cv-section-title">Experiencia Laboral</div>
<div id="cv-jobs-container">
<!-- Las experiencias laborales se mostrarán aquí -->
<div class="cv-item">
<div class="cv-item-title">Puesto de Trabajo</div>
<div class="cv-item-subtitle">Empresa</div>
<div class="cv-item-dates">
Fechas
<span class="current-badge">Actual</span>
</div>
<div>Descripción de responsabilidades y logros en este puesto.</div>
</div>
</div>
</div>
<div class="cv-section">
<div class="cv-section-title">Educación</div>
<div class="cv-item">
<div class="cv-item-title" id="cv-education1-degree">Título Académico</div>
<div class="cv-item-subtitle" id="cv-education1-school">Institución</div>
<div class="cv-item-dates" id="cv-education1-dates">Fechas</div>
</div>
</div>
<div class="cv-section">
<div class="cv-section-title">Habilidades</div>
<div class="cv-skills" id="cv-skills">
<div class="cv-skill">Ejemplo de habilidad</div>
<div class="cv-skill">Otra habilidad</div>
<div class="cv-skill">Habilidad técnica</div>
</div>
</div>
</div>
<div class="actions">
<button class="btn" id="reset-form">
<i class="fas fa-redo"></i> Reiniciar Formulario
</button>
<button class="btn" id="print-cv">
<i class="fas fa-print"></i> Imprimir CV
</button>
</div>
<div class="instructions">
<h3>Instrucciones:</h3>
<ul>
<li><strong>Foto opcional:</strong> Con foto tendrás un CV moderno, sin foto un CV clásico.</li>
<li>Completa todos los campos del formulario.</li>
<li>Agrega tus experiencias laborales con el botón "Agregar Experiencia Laboral".</li>
<li>Marca como "Trabajo Actual" si es tu empleo actual.</li>
<li>La vista previa se actualizará automáticamente.</li>
<li>Agrega tus habilidades una por una.</li>
<li>Haz clic en "Descargar CV en PDF" para guardar tu currículum.</li>
<li>Usa "Imprimir CV" si prefieres imprimirlo directamente.</li>
</ul>
</div>
</section>
</div>
</div>
<!-- Incluir html2pdf -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
// Elementos del formulario
const formInputs = {
name: document.getElementById('name'),
title: document.getElementById('title'),
email: document.getElementById('email'),
phone: document.getElementById('phone'),
location: document.getElementById('location'),
summary: document.getElementById('summary'),
education1Degree: document.getElementById('education1-degree'),
education1School: document.getElementById('education1-school'),
education1Dates: document.getElementById('education1-dates'),
newSkill: document.getElementById('new-skill')
};
// Elementos de vista previa
const previewElements = {
name: document.getElementById('cv-name'),
title: document.getElementById('cv-title'),
email: document.getElementById('cv-email'),
phone: document.getElementById('cv-phone'),
location: document.getElementById('cv-location'),
summary: document.getElementById('cv-summary'),
education1Degree: document.getElementById('cv-education1-degree'),
education1School: document.getElementById('cv-education1-school'),
education1Dates: document.getElementById('cv-education1-dates'),
skills: document.getElementById('cv-skills'),
jobsContainer: document.getElementById('cv-jobs-container'),
headerContainer: document.getElementById('cv-header-container')
};
// Elementos de foto
const photoInput = document.getElementById('photo-input');
const photoPreview = document.getElementById('photo-preview-img');
const photoPlaceholder = document.getElementById('photo-placeholder');
const uploadPhotoBtn = document.getElementById('upload-photo-btn');
const removePhotoBtn = document.getElementById('remove-photo-btn');
// Elementos de habilidades
const skillsContainer = document.getElementById('skills-container');
const addSkillBtn = document.getElementById('add-skill-btn');
const cvSkillsContainer = document.getElementById('cv-skills');
// Elementos de experiencia laboral
const jobsContainer = document.getElementById('jobs-container');
const addJobBtn = document.getElementById('add-job-btn');
// Botones de acción
const generatePdfBtn = document.getElementById('generate-pdf');
const resetFormBtn = document.getElementById('reset-form');
const printCvBtn = document.getElementById('print-cv');
// Almacenar habilidades
let skills = ['JavaScript', 'HTML/CSS', 'React', 'Git'];
// Almacenar experiencias laborales
let jobs = [
{
id: 1,
title: 'Desarrollador Frontend',
company: 'Tech Solutions S.A.',
dates: 'Enero 2020 - Presente',
description: 'Desarrollo de aplicaciones web utilizando React, HTML5 y CSS3. Colaboración en equipos ágiles.',
current: true
},
{
id: 2,
title: 'Desarrollador Web Junior',
company: 'Digital Agency',
dates: 'Junio 2018 - Diciembre 2019',
description: 'Creación de sitios web responsivos y mantenimiento de sitios existentes.',
current: false
}
];
// Almacenar foto (como Data URL)
let userPhoto = null;
// Función para actualizar el encabezado según si hay foto o no
function updateCVHeader() {
if (userPhoto) {
// Con foto: usar diseño con foto
previewElements.headerContainer.innerHTML = `
<div class="cv-header-with-photo">
<div class="cv-header-content">
<div class="cv-name">${formInputs.name.value || 'Nombre Completo'}</div>
<div class="cv-title">${formInputs.title.value || 'Título Profesional'}</div>
<div class="cv-contact">
<div class="cv-contact-item">
<i class="fas fa-envelope"></i> ${formInputs.email.value || 'correo@ejemplo.com'}
</div>
<div class="cv-contact-item">
<i class="fas fa-phone"></i> ${formInputs.phone.value || '+34 612 345 678'}
</div>
<div class="cv-contact-item">
<i class="fas fa-map-marker-alt"></i> ${formInputs.location.value || 'Ciudad, País'}
</div>
</div>
</div>
<div class="cv-photo-container">
<img src="${userPhoto}" class="cv-photo">
</div>
</div>
`;
} else {
// Sin foto: usar diseño clásico (como la versión anterior)
previewElements.headerContainer.innerHTML = `
<div class="cv-header-no-photo">
<div class="cv-name">${formInputs.name.value || 'Nombre Completo'}</div>
<div class="cv-title">${formInputs.title.value || 'Título Profesional'}</div>
<div class="cv-contact">
<div class="cv-contact-item">
<i class="fas fa-envelope"></i> ${formInputs.email.value || 'correo@ejemplo.com'}
</div>
<div class="cv-contact-item">
<i class="fas fa-phone"></i> ${formInputs.phone.value || '+34 612 345 678'}
</div>
<div class="cv-contact-item">
<i class="fas fa-map-marker-alt"></i> ${formInputs.location.value || 'Ciudad, País'}
</div>
</div>
</div>
`;
}
}
// Inicializar foto
function initializePhoto() {
// Agregar evento al botón de subida personalizado
uploadPhotoBtn.addEventListener('click', function() {
photoInput.click();
});
// Manejar selección de archivo
photoInput.addEventListener('change', handlePhotoUpload);
// Manejar eliminación de foto
removePhotoBtn.addEventListener('click', removePhoto);
}
// Manejar subida de foto
function handlePhotoUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Validar tipo de archivo
if (!file.type.match('image.*')) {
alert('Por favor, selecciona un archivo de imagen (JPG, PNG, etc.)');
return;
}
// Validar tamaño (máximo 2MB)
if (file.size > 2 * 1024 * 1024) {
alert('La imagen es demasiado grande. El tamaño máximo es 2MB.');
return;
}
const reader = new FileReader();
reader.onload = function(e) {
userPhoto = e.target.result;
// Mostrar vista previa en el formulario
photoPreview.src = userPhoto;
photoPreview.style.display = 'block';
photoPlaceholder.style.display = 'none';
// Mostrar botón de eliminar
removePhotoBtn.style.display = 'block';
// Actualizar encabezado del CV
updateCVHeader();
};
reader.readAsDataURL(file);
}
// Eliminar foto
function removePhoto() {
userPhoto = null;
// Ocultar vista previa en el formulario
photoPreview.style.display = 'none';
photoPlaceholder.style.display = 'flex';
// Ocultar botón de eliminar
removePhotoBtn.style.display = 'none';
// Limpiar input de archivo
photoInput.value = '';
// Actualizar encabezado del CV
updateCVHeader();
}
// Inicializar experiencias laborales
function initializeJobs() {
jobsContainer.innerHTML = '';
jobs.forEach(job => {
addJobToForm(job);
});
updateJobsPreview();
}
// Agregar experiencia laboral al formulario
function addJobToForm(job = null) {
const jobId = job ? job.id : Date.now();
const jobTitle = job ? job.title : '';
const jobCompany = job ? job.company : '';
const jobDates = job ? job.dates : '';
const jobDescription = job ? job.description : '';
const isCurrent = job ? job.current : false;
const jobEntry = document.createElement('div');
jobEntry.className = 'job-entry';
jobEntry.id = `job-${jobId}`;
jobEntry.innerHTML = `
<div class="job-header">
<div class="job-title">Experiencia Laboral</div>
<button type="button" class="remove-job" data-job-id="${jobId}">
<i class="fas fa-times"></i>
</button>
</div>
<div class="current-job-toggle">
<span class="toggle-label">¿Es tu trabajo actual?</span>
<label class="toggle-switch">
<input type="checkbox" class="current-job-checkbox" ${isCurrent ? 'checked' : ''} data-job-id="${jobId}">
<span class="toggle-slider"></span>
</label>
</div>
<div class="form-group">
<label for="job-title-${jobId}">Puesto de Trabajo</label>
<input type="text" id="job-title-${jobId}" class="job-title-input"
placeholder="Ej: Desarrollador Frontend" value="${jobTitle}">
</div>
<div class="form-group">
<label for="job-company-${jobId}">Empresa</label>
<input type="text" id="job-company-${jobId}" class="job-company-input"
placeholder="Ej: Tech Solutions S.A." value="${jobCompany}">
</div>
<div class="form-group">
<label for="job-dates-${jobId}">Fechas</label>
<input type="text" id="job-dates-${jobId}" class="job-dates-input"
placeholder="Ej: Enero 2020 - Presente" value="${jobDates}">
</div>
<div class="form-group">
<label for="job-description-${jobId}">Descripción</label>
<textarea id="job-description-${jobId}" class="job-description-input"
placeholder="Describe tus responsabilidades y logros en este puesto...">${jobDescription}</textarea>
</div>
`;
jobsContainer.appendChild(jobEntry);
// Añadir eventos a los campos de este trabajo
const titleInput = jobEntry.querySelector('.job-title-input');
const companyInput = jobEntry.querySelector('.job-company-input');
const datesInput = jobEntry.querySelector('.job-dates-input');
const descriptionInput = jobEntry.querySelector('.job-description-input');
const currentCheckbox = jobEntry.querySelector('.current-job-checkbox');
const removeBtn = jobEntry.querySelector('.remove-job');
// Actualizar el array de trabajos cuando cambien los inputs
const updateJobInArray = () => {
const jobIndex = jobs.findIndex(j => j.id === jobId);
if (jobIndex === -1) {
// Nuevo trabajo
jobs.push({
id: jobId,
title: titleInput.value,
company: companyInput.value,
dates: datesInput.value,
description: descriptionInput.value,
current: currentCheckbox.checked
});
} else {
// Actualizar trabajo existente
jobs[jobIndex] = {
...jobs[jobIndex],
title: titleInput.value,
company: companyInput.value,
dates: datesInput.value,
description: descriptionInput.value,
current: currentCheckbox.checked
};
}
updateJobsPreview();
};
titleInput.addEventListener('input', updateJobInArray);
companyInput.addEventListener('input', updateJobInArray);
datesInput.addEventListener('input', updateJobInArray);
descriptionInput.addEventListener('input', updateJobInArray);
currentCheckbox.addEventListener('change', updateJobInArray);
// Eliminar trabajo
removeBtn.addEventListener('click', function() {
if (jobs.length <= 1) {
alert('Debes tener al menos una experiencia laboral.');
return;
}
if (confirm('¿Estás seguro de que quieres eliminar esta experiencia laboral?')) {
jobs = jobs.filter(j => j.id !== jobId);
jobEntry.remove();
updateJobsPreview();
}
});
// Si es un trabajo nuevo, agregarlo al array
if (!job) {
jobs.push({
id: jobId,
title: '',
company: '',
dates: '',
description: '',
current: false
});
}
}
// Actualizar vista previa de experiencias laborales
function updateJobsPreview() {
previewElements.jobsContainer.innerHTML = '';
// Ordenar trabajos: primero los actuales, luego los anteriores
const sortedJobs = [...jobs].sort((a, b) => {
if (a.current && !b.current) return -1;
if (!a.current && b.current) return 1;
return 0;
});
sortedJobs.forEach(job => {
if (job.title || job.company || job.dates || job.description) {
const jobElement = document.createElement('div');
jobElement.className = 'cv-item';
jobElement.innerHTML = `
<div class="cv-item-title">${job.title || 'Puesto de Trabajo'}</div>
<div class="cv-item-subtitle">${job.company || 'Empresa'}</div>
<div class="cv-item-dates">
${job.dates || 'Fechas'}
${job.current ? '<span class="current-badge">Actual</span>' : ''}
</div>
<div>${job.description || 'Descripción de responsabilidades y logros en este puesto.'}</div>
`;
previewElements.jobsContainer.appendChild(jobElement);
}
});
// Si no hay trabajos, mostrar un placeholder
if (previewElements.jobsContainer.children.length === 0) {
const placeholder = document.createElement('div');
placeholder.className = 'cv-item';
placeholder.innerHTML = `
<div class="cv-item-title">Puesto de Trabajo</div>
<div class="cv-item-subtitle">Empresa</div>
<div class="cv-item-dates">
Fechas
<span class="current-badge">Actual</span>
</div>
<div>Descripción de responsabilidades y logros en este puesto.</div>
`;
previewElements.jobsContainer.appendChild(placeholder);
}
}
// Inicializar habilidades
function initializeSkills() {
skills.forEach(skill => addSkillToContainers(skill));
}
// Agregar habilidad a ambos contenedores
function addSkillToContainers(skill) {
// Agregar al formulario
const skillTag = document.createElement('div');
skillTag.className = 'skill-tag';
skillTag.innerHTML = `
${skill}
<span class="remove-skill" data-skill="${skill}">×</span>
`;
skillsContainer.appendChild(skillTag);
// Agregar a la vista previa
const cvSkill = document.createElement('div');
cvSkill.className = 'cv-skill';
cvSkill.textContent = skill;
cvSkillsContainer.appendChild(cvSkill);
// Añadir evento para eliminar habilidad
skillTag.querySelector('.remove-skill').addEventListener('click', function() {
const skillToRemove = this.getAttribute('data-skill');
skills = skills.filter(s => s !== skillToRemove);
updateSkillsDisplay();
});
}
// Actualizar visualización de habilidades
function updateSkillsDisplay() {
// Limpiar contenedores
skillsContainer.innerHTML = '';
cvSkillsContainer.innerHTML = '';
// Volver a agregar todas las habilidades
skills.forEach(skill => addSkillToContainers(skill));
}
// Actualizar vista previa en tiempo real
function updatePreview() {
// Información personal
updateCVHeader(); // Actualizar encabezado según si hay foto o no
previewElements.summary.textContent = formInputs.summary.value || 'Breve descripción de tu experiencia, habilidades y objetivos profesionales.';
// Educación
previewElements.education1Degree.textContent = formInputs.education1Degree.value || 'Título Académico';
previewElements.education1School.textContent = formInputs.education1School.value || 'Institución';
previewElements.education1Dates.textContent = formInputs.education1Dates.value || 'Fechas';
// Experiencias laborales ya se actualizan por separado
}
// Añadir eventos de entrada a todos los campos del formulario
Object.values(formInputs).forEach(input => {
if (input && input.tagName !== 'BUTTON') {
input.addEventListener('input', updatePreview);
}
});
// Añadir nueva habilidad
addSkillBtn.addEventListener('click', function() {
const newSkill = formInputs.newSkill.value.trim();
if (newSkill && !skills.includes(newSkill)) {
skills.push(newSkill);
updateSkillsDisplay();
formInputs.newSkill.value = '';
}
});
// Permitir agregar habilidad con Enter
formInputs.newSkill.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addSkillBtn.click();
}
});
// Añadir nueva experiencia laboral
addJobBtn.addEventListener('click', function() {
addJobToForm();
});
// Crear CV limpio para PDF con pie de página personalizado
function createCleanCVForPDF() {
const cleanCV = document.createElement('div');
cleanCV.className = 'cv-pdf-clean';
// Ordenar trabajos: primero los actuales, luego los anteriores
const sortedJobs = [...jobs].sort((a, b) => {
if (a.current && !b.current) return -1;
if (!a.current && b.current) return 1;
return 0;
});
// Filtrar trabajos que tengan al menos un campo completado
const filteredJobs = sortedJobs.filter(job =>
job.title || job.company || job.dates || job.description
);
// Crear encabezado según si hay foto o no
let headerHTML = '';
if (userPhoto) {
// Con foto: diseño moderno
headerHTML = `
<div style="background-color: #2c3e50; color: white; padding: 30px; border-radius: 8px; margin-bottom: 30px; display: flex; justify-content: space-between; align-items: flex-start;">
<div style="flex: 1;">
<div style="font-size: 28pt; font-weight: bold; margin-bottom: 5px;">${formInputs.name.value || 'Nombre Completo'}</div>
<div style="font-size: 18pt; opacity: 0.9; margin-bottom: 15px;">${formInputs.title.value || 'Título Profesional'}</div>
<div style="display: flex; flex-wrap: wrap; gap: 20px; margin-top: 15px;">
<div style="display: flex; align-items: center; gap: 8px; font-size: 11pt;">
<i class="fas fa-envelope"></i> ${formInputs.email.value || 'correo@ejemplo.com'}
</div>
<div style="display: flex; align-items: center; gap: 8px; font-size: 11pt;">
<i class="fas fa-phone"></i> ${formInputs.phone.value || '+34 612 345 678'}
</div>
<div style="display: flex; align-items: center; gap: 8px; font-size: 11pt;">
<i class="fas fa-map-marker-alt"></i> ${formInputs.location.value || 'Ciudad, País'}
</div>
</div>
</div>
<div style="width: 120px; height: 150px; border-radius: 8px; overflow: hidden; border: 3px solid rgba(255, 255, 255, 0.3); margin-left: 20px; flex-shrink: 0;">
<img src="${userPhoto}" style="width: 100%; height: 100%; object-fit: cover;">
</div>
</div>
`;
} else {
// Sin foto: diseño clásico (como versión anterior)
headerHTML = `
<div style="background-color: #2c3e50; color: white; padding: 30px; border-radius: 8px; margin-bottom: 30px;">
<div style="font-size: 28pt; font-weight: bold; margin-bottom: 5px;">${formInputs.name.value || 'Nombre Completo'}</div>
<div style="font-size: 18pt; opacity: 0.9; margin-bottom: 15px;">${formInputs.title.value || 'Título Profesional'}</div>
<div style="display: flex; flex-wrap: wrap; gap: 20px; margin-top: 15px;">
<div style="display: flex; align-items: center; gap: 8px; font-size: 11pt;">
<i class="fas fa-envelope"></i> ${formInputs.email.value || 'correo@ejemplo.com'}
</div>
<div style="display: flex; align-items: center; gap: 8px; font-size: 11pt;">
<i class="fas fa-phone"></i> ${formInputs.phone.value || '+34 612 345 678'}
</div>
<div style="display: flex; align-items: center; gap: 8px; font-size: 11pt;">
<i class="fas fa-map-marker-alt"></i> ${formInputs.location.value || 'Ciudad, País'}
</div>
</div>
</div>
`;
}
cleanCV.innerHTML = `
${headerHTML}
<div class="cv-pdf-section">
<div class="cv-pdf-section-title">Resumen Profesional</div>
<div>${formInputs.summary.value || 'Breve descripción de tu experiencia, habilidades y objetivos profesionales.'}</div>
</div>
<div class="cv-pdf-section">
<div class="cv-pdf-section-title">Experiencia Laboral</div>
${filteredJobs.map(job => `
<div class="cv-pdf-item">
<div class="cv-pdf-item-title">${job.title || 'Puesto de Trabajo'}</div>
<div class="cv-pdf-item-subtitle">${job.company || 'Empresa'}</div>
<div class="cv-pdf-item-dates">
${job.dates || 'Fechas'}
${job.current ? '<span style="background-color: #27ae60; color: white; padding: 2px 8px; border-radius: 12px; font-size: 0.8rem; margin-left: 8px;">Actual</span>' : ''}
</div>
<div>${job.description || 'Descripción de responsabilidades y logros en este puesto.'}</div>
</div>
`).join('')}
</div>
<div class="cv-pdf-section">
<div class="cv-pdf-section-title">Educación</div>
<div class="cv-pdf-item">
<div class="cv-pdf-item-title">${formInputs.education1Degree.value || 'Título Académico'}</div>
<div class="cv-pdf-item-subtitle">${formInputs.education1School.value || 'Institución'}</div>
<div class="cv-pdf-item-dates">${formInputs.education1Dates.value || 'Fechas'}</div>
</div>
</div>
<div class="cv-pdf-section">
<div class="cv-pdf-section-title">Habilidades</div>
<div class="cv-pdf-skills">
${skills.map(skill => `<div class="cv-pdf-skill">${skill}</div>`).join('')}
</div>
</div>
<!-- Pie de página para el PDF -->
<div class="pdf-footer">
<div class="page-number">Página <span class="page"></span> de <span class="total"></span></div>
</div>
`;
// Agregar estilos CSS para el PDF
const style = document.createElement('style');
style.textContent = `
.cv-pdf-clean {
width: 100%;
background-color: white;
padding: 40px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #333;
line-height: 1.6;
position: relative;
min-height: 100vh;
}
.cv-pdf-section {
margin-bottom: 25px;
}
.cv-pdf-section-title {
color: #2c3e50;
border-bottom: 2px solid #4a6491;
padding-bottom: 5px;
margin-bottom: 15px;
font-size: 16pt;
font-weight: bold;
}
.cv-pdf-item {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.cv-pdf-item:last-child {
border-bottom: none;
}
.cv-pdf-item-title {
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 12pt;
}
.cv-pdf-item-subtitle {
color: #4a6491;
font-style: italic;
margin-bottom: 5px;
font-size: 11pt;
}
.cv-pdf-item-dates {
color: #666;
font-size: 10pt;
margin-bottom: 8px;
display: flex;
align-items: center;
}
.cv-pdf-skills {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 60px;
}
.cv-pdf-skill {
background-color: #eef2f7;
padding: 6px 12px;
border-radius: 4px;
font-size: 10pt;
}
.pdf-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 10pt;
color: #666;
padding: 10px;
background-color: white;
border-top: 1px solid #eee;
height: 40px;
}
.page-number {
font-family: Arial, sans-serif;
}
`;
cleanCV.appendChild(style);
return cleanCV;
}
// Generar PDF limpio con numeración de páginas personalizada
generatePdfBtn.addEventListener('click', async function() {
const cleanCV = createCleanCVForPDF();
// Opciones para html2pdf
const options = {
margin: [0.5, 0.8, 0.5, 0.8], // [top, right, bottom, left]
filename: `CV_${formInputs.name.value || 'Mi_CV'}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
logging: false,
windowWidth: 794,
windowHeight: 1123
},
jsPDF: {
unit: 'mm',
format: 'a4',
orientation: 'portrait',
putTotalPages: '{total_pages_count_string}'
},
pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
};
// Configurar el pie de página con números de página
const worker = html2pdf().set(options).from(cleanCV);
worker.toPdf().get('pdf').then(function(pdf) {
const totalPages = pdf.internal.getNumberOfPages();
for (let i = 1; i <= totalPages; i++) {
pdf.setPage(i);
pdf.setFontSize(10);
pdf.setFont("helvetica", "normal");
pdf.text(`Página ${i} de ${totalPages}`, pdf.internal.pageSize.width / 2,
pdf.internal.pageSize.height - 10, {align: 'center'});
}
}).save();
});
// Imprimir CV limpio
printCvBtn.addEventListener('click', function() {
const cleanCV = createCleanCVForPDF();
// Crear ventana de impresión
const printWindow = window.open('', '_blank');
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>Curriculum Vitae - ${formInputs.name.value || 'Mi CV'}</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #333;
line-height: 1.6;
margin: 0;
padding: 20px;
}
.cv-pdf-header {
background-color: #2c3e50;
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 30px;
}
.cv-pdf-header-with-photo {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.cv-pdf-name {
font-size: 28pt;
margin-bottom: 5px;
font-weight: bold;
}
.cv-pdf-title {
font-size: 18pt;
opacity: 0.9;
margin-bottom: 15px;
}
.cv-pdf-contact {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-top: 15px;
}
.cv-pdf-contact-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11pt;
}
.cv-photo-container {
width: 120px;
height: 150px;
border-radius: 8px;
overflow: hidden;
border: 3px solid rgba(255, 255, 255, 0.3);
margin-left: 20px;
}
.cv-photo-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cv-pdf-section {
margin-bottom: 25px;
}
.cv-pdf-section-title {
color: #2c3e50;
border-bottom: 2px solid #4a6491;
padding-bottom: 5px;
margin-bottom: 15px;
font-size: 16pt;
font-weight: bold;
}
.cv-pdf-item {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.cv-pdf-item:last-child {
border-bottom: none;
}
.cv-pdf-item-title {
font-weight: 600;
color: #333;
margin-bottom: 5px;
font-size: 12pt;
}
.cv-pdf-item-subtitle {
color: #4a6491;
font-style: italic;
margin-bottom: 5px;
font-size: 11pt;
}
.cv-pdf-item-dates {
color: #666;
font-size: 10pt;
margin-bottom: 8px;
display: flex;
align-items: center;
}
.cv-pdf-skills {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.cv-pdf-skill {
background-color: #eef2f7;
padding: 6px 12px;
border-radius: 4px;
font-size: 10pt;
}
@media print {
body {
padding: 0;
}
@page {
margin: 20mm;
@bottom-center {
content: "Página " counter(page) " de " counter(pages);
font-family: Arial, sans-serif;
font-size: 10pt;
color: #666;
}
}
}
</style>
</head>
<body>
${cleanCV.innerHTML}
</body>
</html>
`);
printWindow.document.close();
printWindow.focus();
setTimeout(() => {
printWindow.print();
setTimeout(() => {
printWindow.close();
}, 500);
}, 500);
});
// Reiniciar formulario
resetFormBtn.addEventListener('click', function() {
if (confirm('¿Estás seguro de que quieres reiniciar el formulario? Se perderán todos los datos.')) {
// Limpiar campos del formulario
Object.values(formInputs).forEach(input => {
if (input && input.tagName !== 'BUTTON') {
input.value = '';
}
});
// Restablecer habilidades
skills = ['JavaScript', 'HTML/CSS', 'React', 'Git'];
updateSkillsDisplay();
// Restablecer experiencias laborales
jobs = [
{
id: 1,
title: '',
company: '',
dates: '',
description: '',
current: false
}
];
initializeJobs();
// Eliminar foto si existe
if (userPhoto) {
removePhoto();
}
// Actualizar vista previa
updatePreview();
}
});
// Datos de ejemplo para probar
function loadSampleData() {
// Solo cargar si no hay datos en el formulario
if (!formInputs.name.value) {
formInputs.name.value = 'María López García';
formInputs.title.value = 'Desarrolladora Full Stack';
formInputs.email.value = 'maria.lopez@email.com';
formInputs.phone.value = '+34 634 567 890';
formInputs.location.value = 'Barcelona, España';
formInputs.summary.value = 'Desarrolladora con 5 años de experiencia en el desarrollo de aplicaciones web. Especializada en JavaScript, React y Node.js. Apasionada por crear soluciones eficientes y escalables.';
formInputs.education1Degree.value = 'Grado en Ingeniería Informática';
formInputs.education1School.value = 'Universidad Politécnica de Cataluña';
formInputs.education1Dates.value = '2015 - 2019';
// Actualizar vista previa
updatePreview();
}
}
// Inicializar la aplicación
function init() {
initializePhoto();
initializeSkills();
initializeJobs();
updatePreview();
loadSampleData();
}
// Iniciar cuando el DOM esté listo
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>