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 {
+3 -45
View File
@@ -20,6 +20,9 @@ document.addEventListener('DOMContentLoaded', function() {
const toastContainer = document.getElementById('toast-container');
function showToast(message, type = 'info', duration = 5000) {
// Expose globally for inline scripts
window.showToast = showToast;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
@@ -167,51 +170,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
});
// ----- Photo Upload Form -----
const photoForm = document.querySelector('.photo-upload-form');
if (photoForm) {
const submitBtn = photoForm.querySelector('button[type="submit"]');
photoForm.addEventListener('submit', async function(e) {
e.preventDefault();
// Add loading state
if (submitBtn) {
submitBtn.classList.add('btn-loading');
submitBtn.disabled = true;
}
const formData = new FormData(this);
try {
const response = await fetch(this.action, {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showToast('Photo updated successfully!', 'success');
// Reload page to show new photo
setTimeout(() => location.reload(), 1000);
} else {
showToast(data.error || 'Error uploading photo', 'error');
if (submitBtn) {
submitBtn.classList.remove('btn-loading');
submitBtn.disabled = false;
}
}
} catch (error) {
showToast('Error uploading photo', 'error');
if (submitBtn) {
submitBtn.classList.remove('btn-loading');
submitBtn.disabled = false;
}
}
});
}
// ----- Connection Request Buttons -----
const connectForms = document.querySelectorAll('.connect-form');
connectForms.forEach(form => {