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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ 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.47.1] - 2026-04-21

### Fixed
- **Profile-picture cropper modal no longer bleeds through to the form behind it.** The outer dialog was named `.cropper-modal`, which collides with Cropper.js's own `.cropper-modal` class — the library's internal dark overlay (the mask outside the crop box) was inheriting our `position: fixed; inset: 0;` rules and getting stretched across the viewport, pushing the form and the original modal backdrop through the dialog area. Renamed the outer wrapper + inner panel + header to `.pp-crop-modal*` so Cropper.js's internal elements keep their intended sizing and our dialog chrome stays opaque.

## [1.47.0] - 2026-04-21

### Added
- **LinkedIn-style profile-picture adjustment (zoom + pan).** Uploading or re-opening a profile picture now opens a circular cropper (powered by [Cropper.js](https://github.com/fengyuanchen/cropperjs), loaded via CDN) with a drag-to-reposition stage and a zoom slider (1× to 4×). The original upload is preserved untouched on disk; only normalized crop metadata (`offsetX`, `offsetY`, `zoom`) is stored, applied on every render via CSS `object-position` + `transform: scale()`, and honoured by both the admin page and the public read-only page. A new **Adjust** button in the profile modal lets users re-frame the saved picture at any time without re-uploading. New `picture_crop TEXT` column on `profile` with auto-migration; new `PUT /api/profile/picture/crop` endpoint (values are clamped server-side); uploading, selecting, or removing a picture resets the crop to defaults. Crop propagation follows the existing `picture_propagate` flag semantics: ON mirrors into every dataset snapshot, OFF mirrors only into language siblings of the active dataset, exactly mirroring how the filename already propagates. Seven new i18n keys (`form.adjust_picture`, `form.adjust_hint`, `form.zoom`, `modal.adjust_picture`, `btn.reset_crop`, `toast.crop_saved`, `toast.no_picture`) translated across all 8 supported locales. Existing installs render exactly as before because the default crop `{offsetX:0, offsetY:0, zoom:1}` is visually identical to today's centered `object-fit: cover` baseline.

## [1.46.0] - 2026-04-21

### Added
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.46.0",
"version": "1.47.1",
"description": "Professional CV Management System",
"main": "src/server.js",
"scripts": {
Expand Down
30 changes: 30 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
<link rel="stylesheet" href="/shared/styles.css">
<link rel="stylesheet" href="/shared/admin.css">
<!-- Cropper.js for LinkedIn-style profile picture zoom/pan (admin only) -->
<link rel="stylesheet" href="https://unpkg.com/cropperjs@1.6.2/dist/cropper.min.css">
<script defer src="https://unpkg.com/cropperjs@1.6.2/dist/cropper.min.js"></script>
</head>
<body>
<!-- Toolbar -->
Expand Down Expand Up @@ -500,6 +503,33 @@ <h2 class="section-title" data-i18n="section.projects">Featured Projects</h2>
</section>
</div>

<!-- Profile Picture Cropper Modal (LinkedIn-style zoom/pan). Outer classes
use a `pp-crop-` prefix to avoid colliding with Cropper.js's own
`.cropper-modal` class (the dark overlay inside the crop container). -->
<div class="pp-crop-modal no-print" id="cropperModal" style="display:none;" role="dialog" aria-modal="true" aria-labelledby="cropperModalTitle">
<div class="pp-crop-modal-inner">
<div class="pp-crop-modal-header">
<h3 id="cropperModalTitle" data-i18n="modal.adjust_picture">Adjust Profile Picture</h3>
<button type="button" class="modal-close" onclick="closeCropperModal()" aria-label="Close">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div class="cropper-stage">
<img id="cropperImage" alt="">
</div>
<p class="cropper-hint" data-i18n="form.adjust_hint">Drag to reposition, use the slider or pinch to zoom.</p>
<div class="cropper-controls">
<label for="cropperZoom" data-i18n="form.zoom">Zoom</label>
<input type="range" id="cropperZoom" min="1" max="4" step="0.01" value="1">
</div>
<div class="cropper-actions">
<button type="button" class="btn btn-ghost" onclick="resetCropperCrop()" data-i18n="btn.reset_crop">Reset</button>
<button type="button" class="btn btn-ghost" onclick="closeCropperModal()" data-i18n="btn.cancel">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveCropperCrop()" data-i18n="btn.save">Save</button>
</div>
</div>
</div>

<!-- Edit Modal -->
<div class="modal-overlay" id="modalOverlay">
<div class="modal">
Expand Down
62 changes: 62 additions & 0 deletions public/shared/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,68 @@
.profile-preview-initials { font-size: 24px; font-weight: 700; color: var(--white); display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }
.profile-upload-actions { display: flex; flex-direction: column; gap: 8px; }

/* Profile picture cropper modal (LinkedIn-style zoom + pan).
Cropper.js CSS lives on the CDN; these rules style the surrounding chrome
and force the crop box into a circle to match the live profile circle.
The outer classes are prefixed `pp-crop-` to avoid colliding with
Cropper.js's own `.cropper-modal` class (which is its internal dark
overlay outside the crop box). */
.pp-crop-modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.pp-crop-modal-inner {
background: var(--white);
border-radius: 12px;
padding: 20px;
width: min(520px, 94vw);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.35);
}
.pp-crop-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.pp-crop-modal-header h3 { margin: 0; font-size: 18px; }
.cropper-stage {
width: 100%;
height: 360px;
background: #000;
border-radius: 8px;
overflow: hidden;
}
.cropper-stage img { max-width: 100%; display: block; }
.cropper-hint {
margin: 10px 0 0;
font-size: 12px;
color: var(--muted, #666);
}
.cropper-controls {
display: flex;
gap: 10px;
align-items: center;
margin-top: 12px;
}
.cropper-controls label { font-size: 13px; color: var(--muted, #666); }
.cropper-controls input[type=range] { flex: 1; }
.cropper-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
/* Circular crop overlay so the preview matches the 110px profile circle.
These target Cropper.js's own internal elements — keep the class names. */
.cropper-view-box,
.cropper-face { border-radius: 50%; }

/* Company Logo Upload */
.logo-upload-container { display: flex; align-items: center; gap: 16px; }
.logo-upload-preview {
Expand Down
Loading
Loading