Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to CV Manager will be documented in this file.

Format follows [Keep a Changelog](https://keepachangelog.com/), versioning follows [Semantic Versioning](https://semver.org/).

## [1.50.0] - 2026-05-27

### Added
- **Date of birth field on profile** (#167). Common on CVs in many European countries, especially Germany. A new `birthdate` column on the `profile` table (added via automatic migration), edited from the profile modal with a native `<input type="date">` sized to half the modal width (matching the email and phone fields) and forced to a white background so it visually matches the other text inputs. When set, it renders as a contact badge in the admin view and as a localized "Born / Geb. / Né(e) le / …" line on the ATS PDF and static-site exports, formatted via `Intl.DateTimeFormat` in the active locale (e.g. "14. März 1990" in DE, "March 14, 1990" in EN). When empty, nothing renders anywhere. Treated as sensitive PII: the public read-only server strips `birthdate` from `/api/profile`, `/api/datasets/slug/:slug`, and `/api/datasets/id/:id`, and the static-site export also excludes it — so it is never exposed on the public site even when a dataset is explicitly marked public. `form.birthdate`, `form.birthdate_hint`, and `contact.born` translation keys added to all 8 locale files.

### Fixed
- **Project link disappeared in compact print mode** (#168). When the Projects section had the "print compact" toggle on, `#section-projects.print-compact .project-link { display: none !important }` in `public/shared/styles.css` hid the link icon during printing, even though the same link was always rendered by the ATS PDF export and by the non-compact print view. Removed `.project-link` from that selector list so the link icon now appears next to the project title in compact print mode — consistent with the ATS export.

## [1.49.5] - 2026-05-06

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cv-manager",
"version": "1.49.5",
"version": "1.50.0",
"description": "Professional CV Management System",
"main": "src/server.js",
"scripts": {
Expand Down
33 changes: 33 additions & 0 deletions public/shared/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -404,9 +404,42 @@
border-radius: var(--radius-sm);
font-family: inherit;
font-size: 14px;
line-height: 1.2;
color: var(--gray-700);
background: #fff;
transition: all 0.15s ease;
}

/* Normalize native <input type="date"> so it visually matches the other
.form-input text fields. Chromium renders the value through a shadow-DOM
::-webkit-datetime-edit tree with its own line metrics, intrinsic
min-width, padding, and placeholder color — without these overrides the
field looks taller, wider and lighter than the surrounding inputs. */
.form-input-date {
-webkit-appearance: none;
appearance: none;
min-width: 0;
}
.form-input-date::-webkit-datetime-edit,
.form-input-date::-webkit-datetime-edit-fields-wrapper,
.form-input-date::-webkit-datetime-edit-text,
.form-input-date::-webkit-datetime-edit-year-field,
.form-input-date::-webkit-datetime-edit-month-field,
.form-input-date::-webkit-datetime-edit-day-field {
color: inherit;
padding: 0;
line-height: 1.2;
font-family: inherit;
font-size: inherit;
}
.form-input-date::-webkit-calendar-picker-indicator {
opacity: 0.55;
cursor: pointer;
padding: 0;
margin-left: 4px;
}


.form-input:focus, .form-textarea:focus, .form-select:focus {
outline: none;
border-color: var(--primary);
Expand Down
9 changes: 9 additions & 0 deletions public/shared/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -1478,6 +1478,14 @@ function profileForm(d) {
<input type="text" class="form-input" id="f-phone" value="${escapeHtml(d.phone || '')}">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">${t('form.birthdate')}</label>
<input type="date" class="form-input form-input-date" id="f-birthdate" value="${escapeHtml(d.birthdate || '')}" autocomplete="bday">
<div class="form-hint">${t('form.birthdate_hint')}</div>
</div>
<div></div>
</div>
`;
}

Expand Down Expand Up @@ -1757,6 +1765,7 @@ async function saveItem() {
linkedin: val('f-linkedin'),
email: val('f-email'),
phone: val('f-phone'),
birthdate: val('f-birthdate'),
visible: true,
profile_picture_enabled: checked('f-profilePictureEnabled'),
picture_propagate: propagate,
Expand Down
3 changes: 3 additions & 0 deletions public/shared/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@
"form.open_to_work_hint": "Zeigt ein Abzeichen auf Ihrem öffentlichen Lebenslauf",
"form.email": "E-Mail",
"form.phone": "Telefon",
"form.birthdate": "Geburtsdatum",
"form.birthdate_hint": "Nur in der Admin-Ansicht, im Druck und in ATS-Exporten sichtbar. Auf der öffentlichen Seite niemals.",
"contact.born": "Geb.",
"form.job_title": "Berufsbezeichnung",
"form.company": "Unternehmen",
"form.country_code": "Ländercode",
Expand Down
3 changes: 3 additions & 0 deletions public/shared/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@
"form.open_to_work_hint": "Display a badge on your public CV",
"form.email": "Email",
"form.phone": "Phone",
"form.birthdate": "Date of Birth",
"form.birthdate_hint": "Shown on admin view, print, and ATS exports only. Never shown on the public site.",
"contact.born": "Born",
"form.job_title": "Job Title",
"form.company": "Company",
"form.country_code": "Country Code",
Expand Down
3 changes: 3 additions & 0 deletions public/shared/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@
"form.open_to_work_hint": "Mostrar una insignia en tu CV público",
"form.email": "Correo electrónico",
"form.phone": "Teléfono",
"form.birthdate": "Fecha de nacimiento",
"form.birthdate_hint": "Solo se muestra en la vista de admin, impresión y exportaciones ATS. Nunca en el sitio público.",
"contact.born": "Nacido el",
"form.job_title": "Cargo",
"form.company": "Empresa",
"form.country_code": "Código de país",
Expand Down
3 changes: 3 additions & 0 deletions public/shared/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@
"form.open_to_work_hint": "Afficher un badge sur votre CV public",
"form.email": "E-mail",
"form.phone": "Téléphone",
"form.birthdate": "Date de naissance",
"form.birthdate_hint": "Affichée uniquement dans l'interface d'admin, à l'impression et dans les exports ATS. Jamais sur le site public.",
"contact.born": "Né(e) le",
"form.job_title": "Intitulé du poste",
"form.company": "Entreprise",
"form.country_code": "Code pays",
Expand Down
3 changes: 3 additions & 0 deletions public/shared/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@
"form.open_to_work_hint": "Mostra un badge sul tuo CV pubblico",
"form.email": "Email",
"form.phone": "Telefono",
"form.birthdate": "Data di nascita",
"form.birthdate_hint": "Mostrata solo nella vista admin, in stampa e nelle esportazioni ATS. Mai sul sito pubblico.",
"contact.born": "Nato il",
"form.job_title": "Qualifica professionale",
"form.company": "Azienda",
"form.country_code": "Codice paese",
Expand Down
3 changes: 3 additions & 0 deletions public/shared/i18n/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@
"form.open_to_work_hint": "Toon een badge op uw openbare CV",
"form.email": "E-mail",
"form.phone": "Telefoon",
"form.birthdate": "Geboortedatum",
"form.birthdate_hint": "Alleen zichtbaar in admin-weergave, afdruk en ATS-export. Nooit op de openbare site.",
"contact.born": "Geboren",
"form.job_title": "Functietitel",
"form.company": "Bedrijf",
"form.country_code": "Landcode",
Expand Down
3 changes: 3 additions & 0 deletions public/shared/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@
"form.open_to_work_hint": "Exibir um emblema no seu CV público",
"form.email": "Email",
"form.phone": "Telefone",
"form.birthdate": "Data de nascimento",
"form.birthdate_hint": "Exibida apenas na visualização admin, impressão e exportações ATS. Nunca no site público.",
"contact.born": "Nascido em",
"form.job_title": "Cargo",
"form.company": "Empresa",
"form.country_code": "Código do País",
Expand Down
3 changes: 3 additions & 0 deletions public/shared/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@
"form.open_to_work_hint": "在您的公开简历上显示徽章",
"form.email": "电子邮箱",
"form.phone": "电话",
"form.birthdate": "出生日期",
"form.birthdate_hint": "仅在管理视图、打印和 ATS 导出中显示。公开网站上从不显示。",
"contact.born": "出生于",
"form.job_title": "职位名称",
"form.company": "公司",
"form.country_code": "国家代码",
Expand Down
23 changes: 23 additions & 0 deletions public/shared/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const icons = {
email: materialIcon('email', 14),
phone: materialIcon('phone', 14),
location: materialIcon('location_on', 14),
birthday: materialIcon('cake', 14),
linkedin: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect x="2" y="9" width="4" height="12"/><circle cx="4" cy="4" r="2"/></svg>',
languages: materialIcon('language', 14),
link: materialIcon('open_in_new', 14),
Expand Down Expand Up @@ -781,6 +782,24 @@ function formatTimelinePeriod(item) {
return item.period || '';
}

// Format an ISO YYYY-MM-DD birthdate for display using the active locale.
// Returns '' for empty/invalid input so callers can skip rendering entirely.
function formatBirthdate(value, locale) {
if (!value || typeof value !== 'string') return '';
const m = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!m) return '';
const year = Number(m[1]), month = Number(m[2]), day = Number(m[3]);
if (month < 1 || month > 12 || day < 1 || day > 31) return '';
// Construct date in UTC to avoid timezone shifting the day.
const date = new Date(Date.UTC(year, month - 1, day));
const loc = locale || (typeof I18n !== 'undefined' && I18n.locale) || 'en';
try {
return new Intl.DateTimeFormat(loc, { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(date);
} catch (_e) {
return new Intl.DateTimeFormat('en', { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(date);
}
}

// Load Profile (shared between admin and public)
async function loadProfile(includePrivate = false) {
const p = await api('/api/profile');
Expand Down Expand Up @@ -829,6 +848,10 @@ async function loadProfile(includePrivate = false) {
const badges = [];
if (includePrivate && p.email) badges.push(`<a href="mailto:${escapeHtml(p.email)}" class="contact-badge" itemprop="email">${icons.email} ${escapeHtml(p.email)}</a>`);
if (includePrivate && p.phone) badges.push(`<a href="tel:${escapeHtml(p.phone)}" class="contact-badge" itemprop="telephone">${icons.phone} ${escapeHtml(p.phone)}</a>`);
if (includePrivate && p.birthdate) {
const formatted = formatBirthdate(p.birthdate);
if (formatted) badges.push(`<span class="contact-badge" itemprop="birthDate" content="${escapeHtml(p.birthdate)}">${icons.birthday} ${escapeHtml(formatted)}</span>`);
}
if (p.location) badges.push(`<span class="contact-badge" itemprop="address">${icons.location} ${escapeHtml(p.location)}</span>`);
if (p.linkedin) badges.push(`<a href="${escapeHtml(p.linkedin)}" class="contact-badge" target="_blank" rel="noopener" itemprop="url">${icons.linkedin} LinkedIn</a>`);
if (p.languages) badges.push(`<span class="contact-badge">${icons.languages} ${escapeHtml(p.languages)}</span>`);
Expand Down
29 changes: 28 additions & 1 deletion public/shared/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -2029,7 +2029,6 @@ body.has-preview-banner .container {
break-inside: avoid;
}
#section-projects.print-compact .tech-tags,
#section-projects.print-compact .project-link,
#section-projects.print-compact .drag-handle,
#section-projects.print-compact .item-actions { display: none !important; }
#section-projects.print-compact .project-header {
Expand All @@ -2042,6 +2041,34 @@ body.has-preview-banner .container {
font-weight: 700;
margin: 0;
}
/* Compact print: print the project URL as visible text after the title
instead of an icon. The Material icon span is hidden and the anchor's
::after pseudo-element emits the href as text — the resulting PDF link
annotation attaches to actual rendered glyphs, which is the most
reliably clickable form across PDF viewers (incl. iOS Preview).
The anchor itself stays inline so it doesn't force a line break. */
#section-projects.print-compact .project-link {
display: inline;
margin: 0 0 0 4px;
padding: 0;
background: none;
border-radius: 0;
color: var(--gray-600);
font-size: 9px;
font-weight: 400;
text-decoration: none;
vertical-align: baseline;
line-height: inherit;
width: auto;
height: auto;
}
#section-projects.print-compact .project-link svg,
#section-projects.print-compact .project-link .material-symbols-outlined {
display: none !important;
}
#section-projects.print-compact .project-link::after {
content: "(" attr(href) ")";
}
#section-projects.print-compact .project-description {
display: inline;
font-size: 11px;
Expand Down
Loading
Loading