Initial commit: conference app with Flask
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ 'profile'|t }} - NetEvents{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="profile-page">
|
||||
<h1>{{ 'my_profile'|t }}</h1>
|
||||
|
||||
<div class="profile-container">
|
||||
<div class="profile-photo-section">
|
||||
<div class="current-photo" id="photo-container">
|
||||
{% if attendee.profile_picture %}
|
||||
<img src="{{ url_for('static', filename='uploads/' + attendee.profile_picture) }}" alt="{{ 'profile_photo'|t }}" class="profile-img" id="profile-preview">
|
||||
{% else %}
|
||||
<img src="" alt="{{ 'profile_preview'|t }}" class="profile-img" id="profile-preview" style="display: none;">
|
||||
<div class="no-photo" id="no-photo">{{ 'no_photo'|t }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<canvas id="crop-canvas" style="display: none;"></canvas>
|
||||
|
||||
<div class="zoom-controls">
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="adjustZoom(-0.25)">{{ 'zoom_out'|t }}</button>
|
||||
<span id="zoom-level">100%</span>
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="adjustZoom(0.25)">{{ 'zoom_in'|t }}</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick="resetImage()">{{ 'reset'|t }}</button>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('upload_photo') }}" enctype="multipart/form-data" class="photo-upload-form" id="photo-form">
|
||||
<div class="form-group">
|
||||
<label for="photo">{{ 'upload_photo'|t }}</label>
|
||||
<input type="file" id="photo" name="photo" accept="image/*">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm btn-primary" id="upload-btn">{{ 'upload'|t }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('attendee_profile') }}" class="profile-form">
|
||||
<div class="form-group">
|
||||
<label for="first_name">{{ 'first_name'|t }}</label>
|
||||
<input type="text" id="first_name" name="first_name" value="{{ attendee.first_name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="last_name">{{ 'last_name'|t }}</label>
|
||||
<input type="text" id="last_name" name="last_name" value="{{ attendee.last_name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="organisation">{{ 'organisation'|t }}</label>
|
||||
<input type="text" id="organisation" name="organisation" value="{{ attendee.organisation or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="role">{{ 'role_profession'|t }}</label>
|
||||
<input type="text" id="role" name="role" value="{{ attendee.role or '' }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="introduction">{{ 'short_introduction'|t }}</label>
|
||||
<textarea id="introduction" name="introduction" rows="4">{{ attendee.introduction or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{ 'update_profile'|t }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.current-photo {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin-bottom: 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
.profile-img {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
cursor: move;
|
||||
}
|
||||
.no-photo, #no-photo {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var zoomLevel = 1;
|
||||
var panX = 0;
|
||||
var panY = 0;
|
||||
var isDragging = false;
|
||||
var dragStartX = 0;
|
||||
var dragStartY = 0;
|
||||
var originalImage = null;
|
||||
var containerSize = 150;
|
||||
|
||||
var container, preview, photoInput;
|
||||
|
||||
function init() {
|
||||
container = document.getElementById('photo-container');
|
||||
preview = document.getElementById('profile-preview');
|
||||
photoInput = document.getElementById('photo');
|
||||
|
||||
if (photoInput) {
|
||||
photoInput.addEventListener('change', handleFileSelect);
|
||||
}
|
||||
|
||||
if (container && preview) {
|
||||
container.addEventListener('mousedown', startDrag);
|
||||
document.addEventListener('mousemove', doDrag);
|
||||
document.addEventListener('mouseup', endDrag);
|
||||
container.addEventListener('wheel', handleWheel);
|
||||
}
|
||||
|
||||
document.getElementById('photo-form').addEventListener('submit', handleFormSubmit);
|
||||
}
|
||||
|
||||
function handleFileSelect(e) {
|
||||
var file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
var img = new Image();
|
||||
img.onload = function() {
|
||||
originalImage = img;
|
||||
resetImage();
|
||||
preview.src = event.target.result;
|
||||
preview.style.display = 'block';
|
||||
var noPhoto = document.getElementById('no-photo');
|
||||
if (noPhoto) noPhoto.style.display = 'none';
|
||||
updateDisplay();
|
||||
};
|
||||
img.src = event.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function adjustZoom(delta) {
|
||||
var newZoom = zoomLevel + delta;
|
||||
if (newZoom < 1) newZoom = 1;
|
||||
if (newZoom > 3) newZoom = 3;
|
||||
zoomLevel = newZoom;
|
||||
document.getElementById('zoom-level').textContent = Math.round(zoomLevel * 100) + '%';
|
||||
|
||||
if (zoomLevel === 1) {
|
||||
panX = 0;
|
||||
panY = 0;
|
||||
}
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
function resetImage() {
|
||||
zoomLevel = 1;
|
||||
panX = 0;
|
||||
panY = 0;
|
||||
document.getElementById('zoom-level').textContent = '100%';
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
if (!preview || !originalImage) return;
|
||||
|
||||
var img = originalImage;
|
||||
var imgW = img.naturalWidth;
|
||||
var imgH = img.naturalHeight;
|
||||
var imgAspect = imgW / imgH;
|
||||
|
||||
// Calculate display size (object-fit: cover)
|
||||
var dispW, dispH;
|
||||
if (imgAspect > 1) {
|
||||
dispH = containerSize;
|
||||
dispW = dispH * imgAspect;
|
||||
} else {
|
||||
dispW = containerSize;
|
||||
dispH = dispW / imgAspect;
|
||||
}
|
||||
|
||||
// Center offset for object-fit: cover
|
||||
var offsetX = (containerSize - dispW) / 2;
|
||||
var offsetY = (containerSize - dispH) / 2;
|
||||
|
||||
// Apply pan (drag right = see right side of image)
|
||||
var totalX = offsetX + panX;
|
||||
var totalY = offsetY + panY;
|
||||
|
||||
preview.style.width = dispW + 'px';
|
||||
preview.style.height = dispH + 'px';
|
||||
preview.style.left = totalX + 'px';
|
||||
preview.style.top = totalY + 'px';
|
||||
preview.style.transform = 'scale(' + zoomLevel + ')';
|
||||
preview.style.transformOrigin = 'center center';
|
||||
}
|
||||
|
||||
function startDrag(e) {
|
||||
if (!originalImage) return;
|
||||
isDragging = true;
|
||||
dragStartX = e.clientX - panX;
|
||||
dragStartY = e.clientY - panY;
|
||||
preview.style.cursor = 'grabbing';
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function doDrag(e) {
|
||||
if (!isDragging) return;
|
||||
panX = e.clientX - dragStartX;
|
||||
panY = e.clientY - dragStartY;
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
function endDrag() {
|
||||
isDragging = false;
|
||||
if (preview) preview.style.cursor = 'move';
|
||||
}
|
||||
|
||||
function handleWheel(e) {
|
||||
e.preventDefault();
|
||||
if (e.deltaY < 0) {
|
||||
adjustZoom(0.1);
|
||||
} else {
|
||||
adjustZoom(-0.1);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!originalImage) {
|
||||
alert('Please select an image first');
|
||||
return;
|
||||
}
|
||||
|
||||
var canvas = document.getElementById('crop-canvas');
|
||||
var ctx = canvas.getContext('2d');
|
||||
canvas.width = 300;
|
||||
canvas.height = 300;
|
||||
|
||||
var img = originalImage;
|
||||
var imgW = img.naturalWidth;
|
||||
var imgH = img.naturalHeight;
|
||||
var imgAspect = imgW / imgH;
|
||||
|
||||
// Calculate display size (object-fit: cover)
|
||||
var dispW, dispH;
|
||||
if (imgAspect > 1) {
|
||||
dispH = containerSize;
|
||||
dispW = dispH * imgAspect;
|
||||
} else {
|
||||
dispW = containerSize;
|
||||
dispH = dispW / imgAspect;
|
||||
}
|
||||
|
||||
// Centering offsets
|
||||
var offsetX = (containerSize - dispW) / 2;
|
||||
var offsetY = (containerSize - dispH) / 2;
|
||||
|
||||
// Scale factors from display to image coordinates
|
||||
var scaleX = imgW / dispW;
|
||||
var scaleY = imgH / dispH;
|
||||
|
||||
// Source crop region
|
||||
var srcX = (offsetX + panX + containerSize / 2) / (scaleX * zoomLevel) - containerSize / (2 * scaleX);
|
||||
var srcY = (offsetY + panY + containerSize / 2) / (scaleY * zoomLevel) - containerSize / (2 * scaleY);
|
||||
var srcW = containerSize / (scaleX * zoomLevel);
|
||||
var srcH = containerSize / (scaleY * zoomLevel);
|
||||
|
||||
// Clamp to image bounds
|
||||
srcX = Math.max(0, srcX);
|
||||
srcY = Math.max(0, srcY);
|
||||
srcW = Math.min(imgW - srcX, srcW);
|
||||
srcH = Math.min(imgH - srcY, srcH);
|
||||
|
||||
// Draw with circular clip
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.beginPath();
|
||||
ctx.arc(150, 150, 150, 0, Math.PI * 2);
|
||||
ctx.clip();
|
||||
ctx.drawImage(img, srcX, srcY, srcW, srcH, 0, 0, 300, 300);
|
||||
|
||||
canvas.toBlob(function(blob) {
|
||||
var formData = new FormData();
|
||||
formData.append('photo', blob, 'profile.jpg');
|
||||
|
||||
fetch('{{ url_for("upload_photo") }}', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(data) {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.error || 'Upload failed');
|
||||
}
|
||||
}).catch(function(error) {
|
||||
alert('Error uploading photo');
|
||||
});
|
||||
}, 'image/jpeg', 0.9);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user