Job Board — Resume Builder
A candidate profile and resume builder with editable sections for summary, experience, education, skills, and links beside a live preview pane that updates as you type. A completeness meter tracks profile health with status pills, while a visibility toggle controls recruiter discoverability. Add, remove, and drag-reorder entries, manage skill chips, and export a plain-text resume. Clean, scannable, professional job-board styling with soft shadows and accessible controls.
MCP
Code
:root {
--brand: #2563eb;
--brand-d: #1d4ed8;
--brand-50: #eaf1ff;
--ink: #0f172a;
--ink-2: #475569;
--muted: #64748b;
--bg: #f6f8fb;
--surface: #ffffff;
--line: rgba(15, 23, 42, 0.1);
--line-2: rgba(15, 23, 42, 0.18);
--ok: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--new: #2563eb;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-sm: 0 1px 2px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.04);
--sh-md: 0 4px 16px rgba(15, 23, 42, 0.08);
--sh-lg: 0 18px 50px rgba(15, 23, 42, 0.14);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button, input, textarea { font-family: inherit; font-size: inherit; color: inherit; }
/* ---------- Topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 24px;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: saturate(160%) blur(10px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: baseline; gap: 8px; }
.brand-mark { color: var(--brand); font-size: 18px; transform: translateY(1px); }
.brand-name { font-weight: 800; letter-spacing: -0.02em; }
.brand-sub {
font-size: 12px;
font-weight: 600;
color: var(--muted);
padding: 2px 8px;
background: var(--brand-50);
border-radius: 999px;
}
.topbar-actions { display: flex; align-items: center; gap: 14px; }
.visibility { display: flex; align-items: center; gap: 10px; cursor: pointer; user-select: none; }
.visibility-label { font-size: 13px; font-weight: 600; color: var(--ink-2); }
.switch { position: relative; display: inline-flex; }
.switch input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.switch-track {
width: 42px; height: 24px; border-radius: 999px;
background: var(--line-2); transition: background 0.2s ease; display: block;
}
.switch-thumb {
position: absolute; top: 3px; left: 3px;
width: 18px; height: 18px; border-radius: 50%;
background: #fff; box-shadow: var(--sh-sm);
transition: transform 0.2s ease;
}
.switch input:checked + .switch-track { background: var(--ok); }
.switch input:checked + .switch-track .switch-thumb { transform: translateX(18px); }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--brand); outline-offset: 2px; }
/* ---------- Buttons ---------- */
.btn {
border: 1px solid transparent;
border-radius: var(--r-sm);
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.05s ease, color 0.15s ease;
}
.btn:active { transform: translateY(1px); }
.btn-ghost {
background: var(--surface);
border-color: var(--line-2);
color: var(--ink);
padding: 8px 14px;
font-size: 13px;
}
.btn-ghost:hover { border-color: var(--brand); color: var(--brand); }
.btn-add {
background: var(--brand-50);
color: var(--brand-d);
padding: 9px 14px;
font-size: 13px;
}
.btn-add:hover { background: #dde9ff; }
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
gap: 24px;
max-width: 1280px;
margin: 0 auto;
padding: 24px;
}
.editor { display: flex; flex-direction: column; gap: 18px; min-width: 0; }
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--sh-sm);
padding: 18px;
}
/* ---------- Meter ---------- */
.meter-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.meter-title { margin: 0; font-size: 15px; font-weight: 700; }
.meter-sub { margin: 2px 0 0; font-size: 12.5px; color: var(--muted); }
.meter-pct { font-size: 26px; font-weight: 800; letter-spacing: -0.02em; color: var(--brand); }
.meter-bar {
margin: 14px 0 12px;
height: 10px;
background: var(--brand-50);
border-radius: 999px;
overflow: hidden;
}
.meter-fill {
display: block; height: 100%; width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--brand), #38bdf8);
transition: width 0.45s cubic-bezier(.4,0,.2,1);
}
.meter-checks { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; }
.meter-checks li {
font-size: 12px; font-weight: 600;
padding: 4px 10px; border-radius: 999px;
background: #f1f5f9; color: var(--muted);
display: inline-flex; align-items: center; gap: 6px;
}
.meter-checks li::before { content: "○"; font-size: 11px; }
.meter-checks li.done { background: #e7f7ee; color: var(--ok); }
.meter-checks li.done::before { content: "✓"; }
/* ---------- Sections ---------- */
.section { border: 1px solid var(--line); }
.section-title {
font-size: 13px; font-weight: 800; text-transform: uppercase;
letter-spacing: 0.06em; color: var(--ink-2);
padding: 0 6px; margin-bottom: 6px;
display: inline-flex; align-items: center; gap: 8px;
}
.section-count {
font-size: 11px; font-weight: 700; letter-spacing: 0;
background: var(--brand-50); color: var(--brand-d);
padding: 1px 8px; border-radius: 999px;
}
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px; }
.field:last-child { margin-bottom: 0; }
.field > span { font-size: 12px; font-weight: 600; color: var(--ink-2); }
.field input, .field textarea, .entry input, .entry textarea, .skill-input input {
width: 100%;
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 9px 11px;
font-size: 13.5px;
background: #fcfdff;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.field textarea, .entry textarea { resize: vertical; }
.field input:focus, .field textarea:focus,
.entry input:focus, .entry textarea:focus, .skill-input input:focus {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 3px var(--brand-50);
}
/* ---------- Entries (experience/education/links) ---------- */
.entries { display: flex; flex-direction: column; gap: 12px; margin-bottom: 12px; }
.entry {
position: relative;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 14px;
background: #fbfcfe;
}
.entry.dragging { opacity: 0.5; }
.entry.drag-over { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-50); }
.entry-head {
display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
}
.entry-drag {
cursor: grab; color: var(--muted); font-size: 16px; line-height: 1;
padding: 2px 4px; border-radius: 6px; user-select: none;
background: none; border: none;
}
.entry-drag:hover { background: #eef2f7; color: var(--ink-2); }
.entry-label { font-size: 12px; font-weight: 700; color: var(--muted); flex: 1; }
.entry-move { display: flex; gap: 2px; }
.icon-btn {
width: 26px; height: 26px; border-radius: 6px;
border: 1px solid var(--line-2); background: var(--surface);
color: var(--ink-2); cursor: pointer; font-size: 13px;
display: grid; place-items: center; transition: all 0.15s ease;
}
.icon-btn:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
.icon-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.icon-btn.danger:hover { border-color: var(--danger); color: var(--danger); }
.entry-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.entry-grid .full { grid-column: 1 / -1; }
/* ---------- Skills ---------- */
.skill-input { display: flex; gap: 8px; margin-bottom: 12px; }
.skill-input input { flex: 1; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12.5px; font-weight: 600;
padding: 6px 8px 6px 12px;
background: var(--brand-50); color: var(--brand-d);
border-radius: 999px;
}
.chip button {
border: none; background: none; cursor: pointer;
color: var(--brand-d); font-size: 14px; line-height: 1;
width: 18px; height: 18px; border-radius: 50%;
display: grid; place-items: center;
}
.chip button:hover { background: rgba(29, 78, 216, 0.16); }
.chips:empty::after { content: "No skills yet — add a few above."; font-size: 12.5px; color: var(--muted); }
/* ---------- Preview pane ---------- */
.preview-pane { position: sticky; top: 76px; align-self: start; min-width: 0; }
.preview-toolbar {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 12px;
}
.preview-tag {
font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
color: var(--muted);
}
.preview-status { font-size: 12px; font-weight: 600; color: var(--ok); }
.preview-status.hidden { color: var(--muted); }
.resume {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-lg);
padding: 28px;
transition: opacity 0.25s ease, filter 0.25s ease;
}
.resume.is-hidden { opacity: 0.55; filter: grayscale(0.6); }
.resume-head { display: flex; gap: 16px; align-items: center; margin-bottom: 16px; }
.resume-avatar {
width: 58px; height: 58px; flex: 0 0 58px;
border-radius: 16px;
background: linear-gradient(135deg, var(--brand), #38bdf8);
color: #fff; font-weight: 800; font-size: 20px;
display: grid; place-items: center; letter-spacing: 0.02em;
}
.resume-id h1 { margin: 0; font-size: 21px; font-weight: 800; letter-spacing: -0.02em; }
.resume-id > p { margin: 2px 0 0; font-size: 14px; color: var(--brand-d); font-weight: 600; }
.resume-meta { font-size: 12.5px; color: var(--muted); font-weight: 500 !important; }
.resume-meta a { color: var(--muted); text-decoration: none; }
.resume-meta a:hover { color: var(--brand); }
.resume-meta .dot { margin: 0 6px; }
.resume-summary { margin: 0 0 18px; font-size: 13.5px; color: var(--ink-2); }
.resume-block { border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; }
.resume-block:empty, .resume-block.empty { display: none; }
.resume-h {
margin: 0 0 10px; font-size: 11px; font-weight: 800;
text-transform: uppercase; letter-spacing: 0.1em; color: var(--brand-d);
}
.r-item { margin-bottom: 12px; }
.r-item:last-child { margin-bottom: 0; }
.r-item-top { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; }
.r-role { font-size: 14px; font-weight: 700; color: var(--ink); }
.r-org { font-size: 13px; color: var(--ink-2); font-weight: 600; }
.r-dates { font-size: 11.5px; color: var(--muted); white-space: nowrap; font-weight: 600; }
.r-desc { font-size: 12.5px; color: var(--ink-2); margin: 4px 0 0; }
.resume-skills { display: flex; flex-wrap: wrap; gap: 7px; }
.resume-skills span {
font-size: 12px; font-weight: 600;
padding: 4px 10px; border-radius: 999px;
background: #f1f5f9; color: var(--ink-2);
}
.resume-links { display: flex; flex-wrap: wrap; gap: 8px 16px; }
.resume-links a {
font-size: 12.5px; font-weight: 600; color: var(--brand-d); text-decoration: none;
display: inline-flex; align-items: center; gap: 5px;
}
.resume-links a:hover { text-decoration: underline; }
.resume-links a::before { content: "↗"; font-size: 11px; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 11px 18px;
border-radius: var(--r-sm);
box-shadow: var(--sh-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 920px) {
.layout { grid-template-columns: 1fr; }
.preview-pane { position: static; }
}
@media (max-width: 520px) {
.topbar { padding: 10px 14px; flex-wrap: wrap; }
.visibility-label { display: none; }
.layout { padding: 14px; gap: 16px; }
.card { padding: 14px; }
.grid-2, .entry-grid { grid-template-columns: 1fr; }
.resume { padding: 18px; }
.resume-head { gap: 12px; }
.r-item-top { flex-direction: column; gap: 2px; }
.brand-sub { display: none; }
}(function () {
"use strict";
/* ---------------- State ---------------- */
var state = {
name: "Mara Velez",
title: "Senior Product Designer",
location: "Lisbon, Portugal",
email: "mara.velez@inboxly.io",
summary:
"Product designer with 8 years shaping fintech and marketplace experiences. I turn fuzzy problems into shippable, measurable interfaces — and bring engineers and PMs along for the ride.",
visible: true,
experience: [
{
id: uid(),
role: "Senior Product Designer",
org: "Northwind Pay",
start: "2021",
end: "Present",
desc: "Lead designer for the merchant dashboard. Cut onboarding drop-off 34% with a redesigned KYC flow.",
},
{
id: uid(),
role: "Product Designer",
org: "Marketplace Labs",
start: "2018",
end: "2021",
desc: "Owned the seller experience across web and mobile. Built the first shared component library.",
},
],
education: [
{
id: uid(),
role: "BFA, Interaction Design",
org: "Lisbon School of Design",
start: "2012",
end: "2016",
desc: "Graduated with honors. Thesis on accessible color systems.",
},
],
skills: ["Figma", "Design Systems", "User Research", "Prototyping", "HTML/CSS", "Accessibility"],
links: [
{ id: uid(), label: "Portfolio", url: "maravelez.design" },
{ id: uid(), label: "LinkedIn", url: "linkedin.com/in/maravelez" },
],
};
var LIST_FIELDS = {
experience: [
{ key: "role", label: "Role / title", placeholder: "Senior Product Designer" },
{ key: "org", label: "Company", placeholder: "Northwind Pay" },
{ key: "start", label: "Start", placeholder: "2021", half: true },
{ key: "end", label: "End", placeholder: "Present", half: true },
{ key: "desc", label: "Highlights", placeholder: "What you achieved…", area: true, full: true },
],
education: [
{ key: "role", label: "Degree / program", placeholder: "BFA, Interaction Design" },
{ key: "org", label: "School", placeholder: "Lisbon School of Design" },
{ key: "start", label: "Start", placeholder: "2012", half: true },
{ key: "end", label: "End", placeholder: "2016", half: true },
{ key: "desc", label: "Notes", placeholder: "Honors, thesis…", area: true, full: true },
],
links: [
{ key: "label", label: "Label", placeholder: "Portfolio", half: true },
{ key: "url", label: "URL", placeholder: "maravelez.design", half: true },
],
};
function uid() {
return "x" + Math.random().toString(36).slice(2, 9);
}
/* ---------------- Toast ---------------- */
var toastEl = document.getElementById("toast");
var toastT;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastT);
toastT = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
/* ---------------- Basics binding ---------------- */
document.querySelectorAll("[data-bind]").forEach(function (el) {
el.addEventListener("input", function () {
state[el.getAttribute("data-bind")] = el.value;
renderPreview();
renderMeter();
});
});
/* ---------------- Entry editors ---------------- */
function renderList(type) {
var wrap = document.querySelector('[data-list="' + type + '"]');
var fields = LIST_FIELDS[type];
var items = state[type];
wrap.innerHTML = "";
items.forEach(function (item, idx) {
var entry = document.createElement("div");
entry.className = "entry";
entry.setAttribute("draggable", "true");
entry.dataset.id = item.id;
var gridHtml = fields
.map(function (f) {
var cls = "field" + (f.full ? " full" : "");
var control = f.area
? '<textarea rows="2" data-key="' + f.key + '" placeholder="' + f.placeholder + '"></textarea>'
: '<input type="text" data-key="' + f.key + '" placeholder="' + f.placeholder + '" />';
return '<label class="' + cls + '"><span>' + f.label + "</span>" + control + "</label>";
})
.join("");
entry.innerHTML =
'<div class="entry-head">' +
'<button type="button" class="entry-drag" title="Drag to reorder" aria-hidden="true">⠿</button>' +
'<span class="entry-label">' + labelFor(type) + " " + (idx + 1) + "</span>" +
'<div class="entry-move">' +
'<button type="button" class="icon-btn" data-move="up" title="Move up"' + (idx === 0 ? " disabled" : "") + ">↑</button>" +
'<button type="button" class="icon-btn" data-move="down" title="Move down"' + (idx === items.length - 1 ? " disabled" : "") + ">↓</button>" +
'<button type="button" class="icon-btn danger" data-remove title="Remove">✕</button>' +
"</div></div>" +
'<div class="entry-grid">' + gridHtml + "</div>";
// populate values
entry.querySelectorAll("[data-key]").forEach(function (ctrl) {
ctrl.value = item[ctrl.getAttribute("data-key")] || "";
ctrl.addEventListener("input", function () {
item[ctrl.getAttribute("data-key")] = ctrl.value;
renderPreview();
renderMeter();
});
});
// move / remove
entry.querySelector('[data-move="up"]').addEventListener("click", function () {
move(type, idx, -1);
});
entry.querySelector('[data-move="down"]').addEventListener("click", function () {
move(type, idx, 1);
});
entry.querySelector("[data-remove]").addEventListener("click", function () {
state[type].splice(idx, 1);
renderList(type);
renderPreview();
renderMeter();
toast(labelFor(type) + " removed");
});
attachDrag(entry, type);
wrap.appendChild(entry);
});
var countEl = document.querySelector('[data-count="' + type + '"]');
if (countEl) countEl.textContent = items.length;
}
function labelFor(type) {
return { experience: "Experience", education: "Education", links: "Link" }[type];
}
function move(type, idx, dir) {
var arr = state[type];
var t = idx + dir;
if (t < 0 || t >= arr.length) return;
var tmp = arr[idx];
arr[idx] = arr[t];
arr[t] = tmp;
renderList(type);
renderPreview();
}
/* ---------------- Drag & drop reorder ---------------- */
var dragId = null;
var dragType = null;
function attachDrag(entry, type) {
entry.addEventListener("dragstart", function () {
dragId = entry.dataset.id;
dragType = type;
entry.classList.add("dragging");
});
entry.addEventListener("dragend", function () {
entry.classList.remove("dragging");
document.querySelectorAll(".drag-over").forEach(function (e) {
e.classList.remove("drag-over");
});
dragId = null;
dragType = null;
});
entry.addEventListener("dragover", function (e) {
if (dragType !== type) return;
e.preventDefault();
entry.classList.add("drag-over");
});
entry.addEventListener("dragleave", function () {
entry.classList.remove("drag-over");
});
entry.addEventListener("drop", function (e) {
if (dragType !== type) return;
e.preventDefault();
entry.classList.remove("drag-over");
var arr = state[type];
var from = arr.findIndex(function (i) { return i.id === dragId; });
var to = arr.findIndex(function (i) { return i.id === entry.dataset.id; });
if (from < 0 || to < 0 || from === to) return;
var moved = arr.splice(from, 1)[0];
arr.splice(to, 0, moved);
renderList(type);
renderPreview();
toast("Reordered");
});
}
/* ---------------- Add entry buttons ---------------- */
document.querySelectorAll("[data-add]").forEach(function (btn) {
btn.addEventListener("click", function () {
var type = btn.getAttribute("data-add");
var blank = { id: uid() };
LIST_FIELDS[type].forEach(function (f) {
blank[f.key] = "";
});
state[type].push(blank);
renderList(type);
renderPreview();
renderMeter();
// focus first input of new entry
var entries = document.querySelector('[data-list="' + type + '"]').children;
var last = entries[entries.length - 1];
if (last) last.querySelector("input,textarea").focus();
});
});
/* ---------------- Skills ---------------- */
var skillEntry = document.getElementById("skillEntry");
function addSkill() {
var v = skillEntry.value.trim();
if (!v) return;
if (state.skills.some(function (s) { return s.toLowerCase() === v.toLowerCase(); })) {
toast("Already added");
skillEntry.value = "";
return;
}
state.skills.push(v);
skillEntry.value = "";
renderSkills();
renderPreview();
renderMeter();
}
document.getElementById("skillAdd").addEventListener("click", addSkill);
skillEntry.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
addSkill();
}
});
function renderSkills() {
var wrap = document.getElementById("skillChips");
wrap.innerHTML = "";
state.skills.forEach(function (s, i) {
var chip = document.createElement("span");
chip.className = "chip";
chip.innerHTML = esc(s) + '<button type="button" aria-label="Remove ' + esc(s) + '">×</button>';
chip.querySelector("button").addEventListener("click", function () {
state.skills.splice(i, 1);
renderSkills();
renderPreview();
renderMeter();
});
wrap.appendChild(chip);
});
}
/* ---------------- Preview ---------------- */
function initials(name) {
return (name || "?")
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map(function (w) { return w[0].toUpperCase(); })
.join("") || "?";
}
function renderPreview() {
document.getElementById("rName").textContent = state.name || "Your name";
document.getElementById("rTitle").textContent = state.title || "Your headline";
document.getElementById("rLocation").textContent = state.location || "Location";
var emailEl = document.getElementById("rEmail");
emailEl.textContent = state.email || "email@example.com";
document.getElementById("rAvatar").textContent = initials(state.name);
document.getElementById("rSummary").textContent = state.summary || "";
// experience
var exp = document.getElementById("rExp");
exp.innerHTML = state.experience
.filter(function (e) { return e.role || e.org; })
.map(itemHtml)
.join("");
toggleBlock("rExpBlock", exp.innerHTML);
var edu = document.getElementById("rEdu");
edu.innerHTML = state.education
.filter(function (e) { return e.role || e.org; })
.map(itemHtml)
.join("");
toggleBlock("rEduBlock", edu.innerHTML);
var sk = document.getElementById("rSkills");
sk.innerHTML = state.skills.map(function (s) { return "<span>" + esc(s) + "</span>"; }).join("");
toggleBlock("rSkillBlock", sk.innerHTML);
var ln = document.getElementById("rLinks");
ln.innerHTML = state.links
.filter(function (l) { return l.label || l.url; })
.map(function (l) {
return '<a href="#" title="' + esc(l.url) + '">' + esc(l.label || l.url) + "</a>";
})
.join("");
toggleBlock("rLinkBlock", ln.innerHTML);
}
function itemHtml(e) {
var dates = [e.start, e.end].filter(Boolean).join(" – ");
return (
'<div class="r-item">' +
'<div class="r-item-top">' +
'<span class="r-role">' + esc(e.role || "Untitled") + "</span>" +
(dates ? '<span class="r-dates">' + esc(dates) + "</span>" : "") +
"</div>" +
(e.org ? '<div class="r-org">' + esc(e.org) + "</div>" : "") +
(e.desc ? '<p class="r-desc">' + esc(e.desc) + "</p>" : "") +
"</div>"
);
}
function toggleBlock(id, html) {
document.getElementById(id).classList.toggle("empty", !html.trim());
}
/* ---------------- Completeness meter ---------------- */
var CHECKS = [
{ label: "Headline", ok: function () { return !!state.title.trim(); } },
{ label: "Summary", ok: function () { return state.summary.trim().length >= 40; } },
{ label: "Experience", ok: function () { return state.experience.some(function (e) { return e.role && e.org; }); } },
{ label: "Education", ok: function () { return state.education.some(function (e) { return e.role; }); } },
{ label: "5+ skills", ok: function () { return state.skills.length >= 5; } },
{ label: "Links", ok: function () { return state.links.some(function (l) { return l.url; }); } },
];
function renderMeter() {
var done = CHECKS.filter(function (c) { return c.ok(); }).length;
var pct = Math.round((done / CHECKS.length) * 100);
document.getElementById("meterPct").textContent = pct + "%";
document.getElementById("meterFill").style.width = pct + "%";
var wrap = document.getElementById("meterBarWrap");
wrap.setAttribute("aria-valuenow", String(pct));
var hint = document.getElementById("meterHint");
if (pct === 100) hint.textContent = "Looking sharp — your profile is recruiter-ready.";
else hint.textContent = "A complete profile gets 3× more recruiter views.";
var ul = document.getElementById("meterChecks");
ul.innerHTML = CHECKS.map(function (c) {
return '<li class="' + (c.ok() ? "done" : "") + '">' + c.label + "</li>";
}).join("");
}
/* ---------------- Visibility ---------------- */
var visToggle = document.getElementById("visibilityToggle");
visToggle.addEventListener("change", function () {
state.visible = visToggle.checked;
var label = document.getElementById("visState");
var resume = document.getElementById("resume");
var status = document.getElementById("previewVis");
if (state.visible) {
label.textContent = "Visible to recruiters";
resume.classList.remove("is-hidden");
status.textContent = "● Visible";
status.classList.remove("hidden");
toast("Profile is now visible to recruiters");
} else {
label.textContent = "Hidden from recruiters";
resume.classList.add("is-hidden");
status.textContent = "● Hidden";
status.classList.add("hidden");
toast("Profile hidden — recruiters can't find you");
}
});
/* ---------------- Download ---------------- */
document.getElementById("downloadBtn").addEventListener("click", function () {
var lines = [];
lines.push(state.name);
lines.push(state.title);
lines.push([state.location, state.email].filter(Boolean).join(" · "));
lines.push("");
if (state.summary) { lines.push(state.summary, ""); }
function block(heading, arr) {
var rows = arr.filter(function (e) { return e.role || e.org; });
if (!rows.length) return;
lines.push("== " + heading.toUpperCase() + " ==");
rows.forEach(function (e) {
var dates = [e.start, e.end].filter(Boolean).join(" - ");
lines.push((e.role || "") + (e.org ? " · " + e.org : "") + (dates ? " (" + dates + ")" : ""));
if (e.desc) lines.push(" " + e.desc);
});
lines.push("");
}
block("Experience", state.experience);
block("Education", state.education);
if (state.skills.length) { lines.push("== SKILLS ==", state.skills.join(", "), ""); }
var links = state.links.filter(function (l) { return l.url; });
if (links.length) {
lines.push("== LINKS ==");
links.forEach(function (l) { lines.push((l.label ? l.label + ": " : "") + l.url); });
}
var blob = new Blob([lines.join("\n")], { type: "text/plain" });
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = (state.name || "resume").toLowerCase().replace(/\s+/g, "-") + "-resume.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () { URL.revokeObjectURL(a.href); }, 1000);
toast("Resume downloaded");
});
/* ---------------- Init ---------------- */
renderList("experience");
renderList("education");
renderList("links");
renderSkills();
renderPreview();
renderMeter();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Resume Builder — Candidate Profile</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◆</span>
<span class="brand-name">HireDeck</span>
<span class="brand-sub">Resume Builder</span>
</div>
<div class="topbar-actions">
<label class="visibility" title="Toggle profile visibility to recruiters">
<span class="visibility-label" id="visState">Visible to recruiters</span>
<span class="switch">
<input type="checkbox" id="visibilityToggle" checked aria-label="Profile visibility" />
<span class="switch-track" aria-hidden="true"><span class="switch-thumb"></span></span>
</span>
</label>
<button class="btn btn-ghost" id="downloadBtn" type="button">↧ Download</button>
</div>
</header>
<main class="layout">
<!-- ============ EDITOR ============ -->
<section class="editor" aria-label="Resume editor">
<div class="meter card">
<div class="meter-head">
<div>
<h2 class="meter-title">Profile completeness</h2>
<p class="meter-sub" id="meterHint">A complete profile gets 3× more recruiter views.</p>
</div>
<strong class="meter-pct" id="meterPct">0%</strong>
</div>
<div class="meter-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" id="meterBarWrap">
<span class="meter-fill" id="meterFill"></span>
</div>
<ul class="meter-checks" id="meterChecks"></ul>
</div>
<!-- Identity -->
<fieldset class="card section" data-section="identity">
<legend class="section-title">Basics</legend>
<div class="grid-2">
<label class="field"><span>Full name</span>
<input type="text" data-bind="name" value="Mara Velez" />
</label>
<label class="field"><span>Headline</span>
<input type="text" data-bind="title" value="Senior Product Designer" />
</label>
<label class="field"><span>Location</span>
<input type="text" data-bind="location" value="Lisbon, Portugal" />
</label>
<label class="field"><span>Email</span>
<input type="email" data-bind="email" value="mara.velez@inboxly.io" />
</label>
</div>
<label class="field"><span>Professional summary</span>
<textarea data-bind="summary" rows="3">Product designer with 8 years shaping fintech and marketplace experiences. I turn fuzzy problems into shippable, measurable interfaces — and bring engineers and PMs along for the ride.</textarea>
</label>
</fieldset>
<!-- Experience -->
<fieldset class="card section" data-section="experience">
<legend class="section-title">Experience <span class="section-count" data-count="experience"></span></legend>
<div class="entries" id="expList" data-list="experience"></div>
<button class="btn btn-add" type="button" data-add="experience">+ Add experience</button>
</fieldset>
<!-- Education -->
<fieldset class="card section" data-section="education">
<legend class="section-title">Education <span class="section-count" data-count="education"></span></legend>
<div class="entries" id="eduList" data-list="education"></div>
<button class="btn btn-add" type="button" data-add="education">+ Add education</button>
</fieldset>
<!-- Skills -->
<fieldset class="card section" data-section="skills">
<legend class="section-title">Skills <span class="section-count" data-count="skills"></span></legend>
<div class="skill-input">
<input type="text" id="skillEntry" placeholder="Add a skill and press Enter…" aria-label="Add skill" />
<button class="btn btn-add" type="button" id="skillAdd">Add</button>
</div>
<div class="chips" id="skillChips"></div>
</fieldset>
<!-- Links -->
<fieldset class="card section" data-section="links">
<legend class="section-title">Links <span class="section-count" data-count="links"></span></legend>
<div class="entries" id="linkList" data-list="links"></div>
<button class="btn btn-add" type="button" data-add="links">+ Add link</button>
</fieldset>
</section>
<!-- ============ PREVIEW ============ -->
<aside class="preview-pane" aria-label="Live resume preview">
<div class="preview-toolbar">
<span class="preview-tag">Live preview</span>
<span class="preview-status" id="previewVis">● Visible</span>
</div>
<article class="resume" id="resume">
<header class="resume-head">
<div class="resume-avatar" id="rAvatar">MV</div>
<div class="resume-id">
<h1 id="rName">Mara Velez</h1>
<p id="rTitle">Senior Product Designer</p>
<p class="resume-meta">
<span id="rLocation">Lisbon, Portugal</span>
<span class="dot">·</span>
<a id="rEmail" href="#">mara.velez@inboxly.io</a>
</p>
</div>
</header>
<p class="resume-summary" id="rSummary"></p>
<section class="resume-block" id="rExpBlock">
<h3 class="resume-h">Experience</h3>
<div id="rExp"></div>
</section>
<section class="resume-block" id="rEduBlock">
<h3 class="resume-h">Education</h3>
<div id="rEdu"></div>
</section>
<section class="resume-block" id="rSkillBlock">
<h3 class="resume-h">Skills</h3>
<div class="resume-skills" id="rSkills"></div>
</section>
<section class="resume-block" id="rLinkBlock">
<h3 class="resume-h">Links</h3>
<div class="resume-links" id="rLinks"></div>
</section>
</article>
</aside>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Resume Builder
A two-pane candidate profile editor in the style of a modern job board. The left column holds editable sections — basics, experience, education, skills, and links — while the right column renders a polished resume card that updates live as you type. A completeness meter at the top scores the profile against six checks (headline, summary, experience, education, five-plus skills, links) and surfaces them as status pills so candidates always know what’s missing.
Every list section supports adding and removing entries, nudging them up or down, and full drag-and-drop reordering — changes flow straight into the preview. Skills are managed as removable chips you add with Enter or the button, with duplicate detection. A header visibility switch toggles whether the profile is “visible to recruiters,” dimming the preview and updating its status badge, and a download action exports a clean plain-text version of the resume.
The build is vanilla HTML, CSS, and JavaScript with no dependencies: CSS variables drive the trustworthy blue-and-slate palette, soft shadows and rounded cards give it a scannable ATS feel, and the layout collapses gracefully from desktop down to roughly 360px. Interactive controls are keyboard-usable and labeled for assistive tech.
Illustrative UI only — fictional jobs & companies, not a real hiring platform.