Add LinkedIn-style profile picture cropper (zoom + pan)#140
Merged
Conversation
Uploading or re-opening a profile picture now opens a circular cropper (Cropper.js via CDN) with drag-to-reposition and a zoom slider. The original upload stays untouched on disk; only normalized crop metadata is stored in a new profile.picture_crop column and applied on render via CSS object-position + transform: scale(). A new "Adjust" button in the profile modal lets users re-frame the saved picture at any time without re-uploading. New PUT /api/profile/picture/crop endpoint (clamps values server-side); uploading/selecting/removing a picture resets the crop. Crop propagation mirrors the existing picture_propagate semantics: ON writes into every dataset, OFF into language siblings only. Seven new i18n keys translated across all 8 locales. Bump to 1.47.0.
The outer dialog was named `.cropper-modal`, which is also the class Cropper.js uses for its internal dark overlay outside the crop box. Our `position: fixed; inset: 0;` and semi-transparent background were being applied to Cropper.js's internal element too, stretching it across the viewport and letting the form behind bleed through the dialog. Renamed the outer wrapper classes to `.pp-crop-modal*` so Cropper.js keeps its intended sizing and our dialog chrome stays opaque. Bump to 1.47.1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a LinkedIn-style profile picture cropper modal that allows users to zoom and pan their profile picture before saving. Users can adjust existing pictures or newly uploaded ones with a visual cropper interface featuring:
The implementation uses Cropper.js (CDN-loaded) for the interactive UI and stores normalized crop values in a new
picture_cropdatabase column.Type of Change
Checklist
Required for all code changes
npm testpasses)package.json,package-lock.json,version.json)CHANGELOG.mdhas been updated with a new entry under the correct versionIf adding or changing user-visible strings
t('key')in JS ordata-i18nin HTMLen.jsonand all 7 other locale files (de,fr,nl,es,it,pt,zh)escapeHtml()used for any user-provided content rendered as HTMLKey Changes
Backend (
src/server.js)picture_crop TEXTcolumn to profile table with migrationnormalizeCrop()function to validate and clamp crop values (offsetX/Y: ±100, zoom: 1–4)PUT /api/profile/picture/crop— save crop metadata with propagation supportGET /api/profileto includepicture_cropfieldapplyToAll: true) or language siblings only (iffalse)Frontend (
public/shared/admin.js)openCropperForNewUpload()— auto-opens cropper after file selection (LinkedIn parity)openCropperForExisting()— opens cropper for the current saved picturereadCropFromCropper()/cropToCropperData()— pure math helpers for crop ↔ Cropper.js data conversionpendingProfilePictureCropand applied server-side after file saveFrontend (
public/shared/scripts.js)applyProfilePictureCrop()helper to render crop via CSS (object-position+transform: scale())UI & Styling (
public/index.html,public/shared/admin.css).pp-crop-to avoid collision with Cropper.js's internal.cropper-modalclassInternationalization
btn.reset_crop,form.adjust_picture,form.adjust_hint,form.zoom,modal.adjust_picture,toast.crop_saved,toast.no_pictureTests
https://claude.ai/code/session_01WcdrYPM5U9jMgcjMUmRsFg