Generator CV Online
Stwórz swoje profesjonalne CV w formacie PDF w kilka minut!
Dzięki prostemu kreatorowi możesz:
- Wprowadzić dane osobowe, doświadczenie, edukację i umiejętności
- Wybrać nowoczesny szablon dopasowany do Twojego stylu
- Oglądać podgląd CV na bieżąco podczas edycji
- Pobierać gotowe CV do formatu PDF jednym kliknięciem
Polityka prywatności
Twoje dane są wprowadzane tylko w przeglądarce – nie są wysyłane na żadne serwery i nie są nigdzie przechowywane.
Masz pełną kontrolę nad swoimi informacjami.
Wypróbuj teraz i stwórz CV, które zrobi wrażenie!
Dane CV
Podgląd CV
Kod po stronie przeglądarki
<style>
.cv-preview {
width: 190mm;
min-height: 270mm;
padding: 24px;
background: white;
color: #222;
box-shadow: 0 0 8px rgba(0,0,0,0.08);
margin: 0 auto;
}
.photo {
width: 96px;
height: 96px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #ddd;
}
.section-title { font-weight: 700; margin-bottom: 8px; }
.muted { color: #666; }
.template-compact .cv-header { display:flex; gap:16px; align-items:center; }
.template-modern .cv-header { border-bottom:4px solid #0d6efd; padding-bottom:12px; margin-bottom:12px }
@media print {
.page-break { page-break-before: always; }
}
</style>
<div id="app">
<div class="row gy-3">
<div class="col-lg-5">
<div class="card">
<div class="card-body">
<h3 class="h4 card-title">Dane CV</h3>
<form id="cvForm" onsubmit="return false;">
<div class="mb-2">
<label for="fullName" class="form-label">Imię i nazwisko</label>
<input id="fullName" class="form-control" placeholder="Jan Kowalski">
</div>
<div class="mb-2 row">
<div class="col-8">
<label for="title" class="form-label">Stanowisko / nagłówek</label>
<input id="title" class="form-control" placeholder="Frontend Developer">
</div>
<div class="col-4">
<label for="age" class="form-label">Wiek</label>
<input id="age" type="number" min="0" class="form-control" placeholder="30">
</div>
</div>
<div class="mb-2">
<label for="contact" class="form-label">Kontakt (email / telefon / miasto)</label>
<input id="contact" class="form-control" placeholder="jan@example.com • +48 600 000 000 • Warszawa">
</div>
<div class="mb-2">
<label for="profile" class="form-label">Profil — krótki opis</label>
<textarea id="profile" rows="3" class="form-control" placeholder="Krótki akapit o Tobie..."></textarea>
</div>
<div class="mb-2">
<label for="photoInput" class="form-label">Zdjęcie (opcjonalnie)</label>
<input id="photoInput" type="file" accept="image/*" class="form-control">
<div class="form-text">Zdjęcie zostanie osadzone w podglądzie CV.</div>
</div>
<hr>
<div class="d-flex justify-content-between align-items-center mb-2">
<h4 class="h6 m-0">Doświadczenie</h4>
<button class="btn btn-sm btn-primary" id="addExpBtn">Dodaj pozycję</button>
</div>
<div id="experienceList"></div>
<hr>
<div class="d-flex justify-content-between align-items-center mb-2">
<h4 class="h6 m-0">Edukacja</h4>
<button class="btn btn-sm btn-primary" id="addEduBtn">Dodaj pozycję</button>
</div>
<div id="educationList"></div>
<hr>
<div class="mb-2">
<label for="skills" class="form-label">Umiejętności (oddziel przecinkami)</label>
<input id="skills" class="form-control" placeholder="JavaScript, HTML, CSS, React">
</div>
<div class="mb-2">
<label for="templateSelect" class="form-label">Szablon</label>
<select id="templateSelect" class="form-select">
<option value="modern">Nowoczesny (niebieski akcent)</option>
<option value="compact">Kompaktowy</option>
</select>
</div>
<div class="d-grid gap-2">
<button id="updatePreview" class="btn btn-primary">Aktualizuj podgląd</button>
<button id="generatePDF" class="btn btn-success">Generuj PDF</button>
</div>
</form>
</div>
</div>
<div class="mt-3 text-muted small">Instrukcja: wypełnij pola, dodaj doświadczenia i edukację. Kliknij "Aktualizuj podgląd", a potem "Generuj PDF".</div>
</div>
<div class="col-lg-7">
<div class="card">
<div class="card-body">
<h3 class="h5 card-title">Podgląd CV</h3>
<div id="previewWrapper" class="border bg-white p-3" style="overflow:auto;">
<div id="cvPreview" class="cv-preview template-modern">
<div class="cv-header d-flex justify-content-between align-items-start">
<div id="cvHeaderText"></div>
<img id="cvPhoto" class="photo d-none" alt="Moje zdjęcie CV">
</div>
<div id="cvContent"></div>
</div>
</div>
</div>
</div>
<div class="mt-2 text-end">
<small class="text-muted">PDF: format A4, borderless wygenerowany po stronie klienta.</small>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.9.3/html2pdf.bundle.min.js"></script>
<script>
const experienceList = document.getElementById('experienceList');
const educationList = document.getElementById('educationList');
const photoInput = document.getElementById('photoInput');
let photoDataUrl = null;
addExperience();
addEducation();
document.getElementById('addExpBtn').addEventListener('click', (e)=>{ e.preventDefault(); addExperience(); });
document.getElementById('addEduBtn').addEventListener('click', (e)=>{ e.preventDefault(); addEducation(); });
document.getElementById('updatePreview').addEventListener('click', (e)=>{ e.preventDefault(); renderPreview(); });
document.getElementById('generatePDF').addEventListener('click', (e)=>{ e.preventDefault(); generatePDF(); });
photoInput.addEventListener('change', function(){
const f = this.files[0];
if (!f) {
photoDataUrl = null;
document.getElementById('cvPhoto').classList.add('d-none');
return;
}
const reader = new FileReader();
reader.onload = function(ev){
photoDataUrl = ev.target.result;
const img = document.getElementById('cvPhoto');
img.src = photoDataUrl;
img.classList.remove('d-none');
};
reader.readAsDataURL(f);
});
function createFieldGroup(html){
const div = document.createElement('div');
div.className = 'mb-2 border rounded p-2';
div.innerHTML = html;
return div;
}
function addExperience(data={position:'',company:'',from:'',to:'',description:''}){
const idx = experienceList.children.length;
const html = `
<div class="d-flex justify-content-between mb-2">
<strong>Pozycja ${idx+1}</strong>
<div>
<button class="btn btn-sm btn-danger remove-exp">Usuń</button>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-2"><input class="form-control exp-position" placeholder="Stanowisko" aria-label="Nazwa stanowiska" value="${escapeHtml(data.position)}"></div>
<div class="col-md-6 mb-2"><input class="form-control exp-company" placeholder="Firma" aria-label="Nazwa firmy" value="${escapeHtml(data.company)}"></div>
<div class="col-md-4 mb-2"><input class="form-control exp-from" placeholder="Od (np. 2021)" aria-label="Od dnia" value="${escapeHtml(data.from)}"></div>
<div class="col-md-4 mb-2"><input class="form-control exp-to" placeholder="Do (np. 2023 / obecnie)" aria-label="Do dnia" value="${escapeHtml(data.to)}"></div>
<div class="col-12 mb-2"><textarea class="form-control exp-desc" rows="2" placeholder="Krótki opis obowiązków" aria-label="Opis obowiązków">${escapeHtml(data.description)}</textarea></div>
</div>
`;
const node = createFieldGroup(html);
node.querySelector('.remove-exp').addEventListener('click', ()=>{ node.remove(); });
experienceList.appendChild(node);
}
function addEducation(data={degree:'',institution:'',from:'',to:''}){
const idx = educationList.children.length;
const html = `
<div class="d-flex justify-content-between mb-2">
<strong>Edukacja ${idx+1}</strong>
<div>
<button class="btn btn-sm btn-danger remove-edu">Usuń</button>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-2"><input class="form-control edu-degree" placeholder="Stopień / kierunek" aria-label="Stopień" value="${escapeHtml(data.degree)}"></div>
<div class="col-md-6 mb-2"><input class="form-control edu-inst" placeholder="Uczelnia / szkoła" aria-label="Uczelnia" value="${escapeHtml(data.institution)}"></div>
<div class="col-md-4 mb-2"><input class="form-control edu-from" placeholder="Od" aria-label="Od dnia" value="${escapeHtml(data.from)}"></div>
<div class="col-md-4 mb-2"><input class="form-control edu-to" placeholder="Do" aria-label="Do dnia" value="${escapeHtml(data.to)}"></div>
</div>
`;
const node = createFieldGroup(html);
node.querySelector('.remove-edu').addEventListener('click', ()=>{ node.remove(); });
educationList.appendChild(node);
}
function escapeHtml(s){
if (!s) return '';
return String(s).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>');
}
function collectForm(){
const data = {};
data.fullName = document.getElementById('fullName').value.trim();
data.title = document.getElementById('title').value.trim();
data.age = document.getElementById('age').value.trim();
data.contact = document.getElementById('contact').value.trim();
data.profile = document.getElementById('profile').value.trim();
data.skills = document.getElementById('skills').value.trim().split(',').map(s=>s.trim()).filter(Boolean);
data.template = document.getElementById('templateSelect').value;
data.experience = Array.from(experienceList.children).map(group => ({
position: group.querySelector('.exp-position')?.value || '',
company: group.querySelector('.exp-company')?.value || '',
from: group.querySelector('.exp-from')?.value || '',
to: group.querySelector('.exp-to')?.value || '',
description: group.querySelector('.exp-desc')?.value || ''
}));
data.education = Array.from(educationList.children).map(group => ({
degree: group.querySelector('.edu-degree')?.value || '',
institution: group.querySelector('.edu-inst')?.value || '',
from: group.querySelector('.edu-from')?.value || '',
to: group.querySelector('.edu-to')?.value || ''
}));
data.photo = photoDataUrl;
return data;
}
function renderPreview(){
const d = collectForm();
const cvPreview = document.getElementById('cvPreview');
cvPreview.className = 'cv-preview ' + (d.template === 'compact' ? 'template-compact' : 'template-modern');
const skillsHtml = d.skills.length ? `<div class="mb-2"><strong class="section-title">Umiejętności</strong><div>${d.skills.map(s=>`<span class="badge bg-light text-dark me-1">${escapeHtml(s)}</span>`).join('')}</div></div>` : '';
const expHtml = d.experience.filter(e=> e.position || e.company).map(e=>`
<div class="mb-2">
<div><strong>${escapeHtml(e.position)}</strong> <span class="muted">— ${escapeHtml(e.company)}</span></div>
<div class="muted small">${escapeHtml(e.from)} — ${escapeHtml(e.to)}</div>
<div class="mt-1">${escapeHtml(e.description)}</div>
</div>
`).join('');
const eduHtml = d.education.filter(e=> e.degree || e.institution).map(e=>`
<div class="mb-2">
<div><strong>${escapeHtml(e.degree)}</strong></div>
<div class="muted small">${escapeHtml(e.institution)} • ${escapeHtml(e.from)} — ${escapeHtml(e.to)}</div>
</div>
`).join('');
const headerText = `
<div style="font-size:20px;font-weight:700">${escapeHtml(d.fullName || 'Imię Nazwisko')}</div>
<div class="muted">${escapeHtml(d.title || '')} ${d.age ? '• ' + escapeHtml(d.age) : ''}</div>
<div class="muted small mt-2">${escapeHtml(d.contact || '')}</div>
`;
document.getElementById('cvHeaderText').innerHTML = headerText;
if (!d.photo) {
document.getElementById('cvPhoto').classList.add('d-none');
} else {
const img = document.getElementById('cvPhoto');
img.src = d.photo;
img.classList.remove('d-none');
}
document.getElementById('cvContent').innerHTML = `
${d.profile ? `<div class="mb-3"><strong class="section-title">Profil</strong><div>${escapeHtml(d.profile)}</div></div>` : ''}
${expHtml ? `<div class="mb-3"><strong class="section-title">Doświadczenie</strong>${expHtml}</div>` : ''}
${eduHtml ? `<div class="mb-3"><strong class="section-title">Edukacja</strong>${eduHtml}</div>` : ''}
${skillsHtml}
`;
}
renderPreview();
function generatePDF(){
const filename = (document.getElementById('fullName').value.trim() || 'CV') + '.pdf';
const opt = {
margin: [8, 8],
filename: filename,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, logging: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
renderPreview();
const prev = document.getElementById('cvPreview');
prev.style.boxShadow = 'none';
html2pdf().set(opt).from(prev).save().finally(()=>{ prev.style.boxShadow = ''; });
}
</script>
Kod po stronie serwera
Informacja
Aplikacja nie korzysta z kodu po stronie serwera.