Redesign profile photo upload with circular crop UI, server-side processing, and form integration

- Replace square preview with 280x280 circular container with crop ring overlay
- Zoom slider and drag-to-pan for precise crop positioning
- Client-side crop via canvas at 400x400 output resolution
- Server-side resize to 400x400 (LANCZOS), EXIF stripping, RGB conversion, JPEG optimization
- Generate CSRF token in before_request so photo upload fetch works
- Save button now also submits the profile form (photo + fields updated together)
- Remove dead photo-upload-form handler and unused CSS
- Clean up debug markers and attendee code field from profile page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 07:10:52 +00:00
parent a3546b4c01
commit 3c26f1310e
4 changed files with 380 additions and 305 deletions
+59 -31
View File
@@ -1373,14 +1373,6 @@ a:focus-visible,
margin-bottom: var(--space-4);
}
.profile-img {
width: 160px;
height: 160px;
border-radius: 50%;
object-fit: cover;
border: 4px solid var(--border-color);
}
.no-photo {
width: 160px;
height: 160px;
@@ -1403,10 +1395,6 @@ a:focus-visible,
fill: none;
}
.photo-upload-form {
margin-top: var(--space-4);
}
/* ----- Attendees Page ----- */
.attendees-page {
padding: 0;
@@ -2886,50 +2874,90 @@ select:focus {
align-items: center;
}
.profile-page .current-photo {
width: 150px;
height: 150px;
margin-bottom: var(--space-4);
/* ----- Circular Crop Container ----- */
.photo-crop-container {
width: 280px;
height: 280px;
border-radius: 50%;
overflow: hidden;
position: relative;
background: var(--border-color);
cursor: move;
margin-bottom: var(--space-4);
}
.profile-page .current-photo .profile-img {
.photo-crop-container .crop-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
cursor: move;
user-select: none;
-webkit-user-drag: none;
}
.profile-page .no-photo {
width: 150px;
height: 150px;
.photo-crop-container .crop-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.85);
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.35);
pointer-events: none;
}
.photo-crop-container .no-photo-circle {
position: absolute;
inset: 0;
border-radius: 50%;
background: var(--bg-color);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: var(--font-size-sm);
border: 3px solid var(--border-color);
}
.zoom-controls {
display: flex;
gap: var(--space-2);
align-items: center;
margin-bottom: var(--space-4);
.photo-crop-container .no-photo-circle svg {
width: 64px;
height: 64px;
}
.zoom-controls span {
/* ----- Zoom Slider ----- */
.crop-slider-group {
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-4);
width: 100%;
max-width: 280px;
}
.crop-slider-icon {
width: 16px;
height: 16px;
color: var(--text-muted);
flex-shrink: 0;
}
.crop-slider {
flex: 1;
accent-color: var(--primary-color);
height: 6px;
cursor: pointer;
}
.crop-slider-group span {
font-size: var(--font-size-sm);
color: var(--text-muted);
min-width: 50px;
min-width: 42px;
text-align: center;
flex-shrink: 0;
}
/* ----- Photo Action Buttons ----- */
.photo-actions {
display: flex;
gap: var(--space-3);
align-items: center;
}
.switch-group {