fixed photo upload

This commit is contained in:
Claudio 2026-01-29 09:01:04 +01:00
parent 2734679938
commit e4b472f0c1

View File

@ -440,6 +440,100 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
transform: scale(1.03); transform: scale(1.03);
box-shadow: 0 10px 25px rgba(0, 0, 0, .16); box-shadow: 0 10px 25px rgba(0, 0, 0, .16);
} }
/* Dropzone states */
#photos-dropzone.is-dragover {
border-color: #0d6efd !important;
background: #eef5ff !important;
transform: scale(1.01);
transition: all .12s ease;
}
#photos-dropzone .dz-hint {
font-weight: 600;
}
#photos-dropzone.is-dragover .dz-hint {
color: #0d6efd;
}
/* Upload queue items */
.upload-item {
display: flex;
gap: 12px;
align-items: center;
padding: 10px 12px;
border: 1px solid #e9ecef;
border-radius: 10px;
background: #fff;
margin-top: 10px;
}
.upload-thumb {
width: 54px;
height: 54px;
border-radius: 10px;
object-fit: cover;
border: 1px solid #dee2e6;
background: #f8f9fa;
}
.upload-meta {
flex: 1;
min-width: 0;
}
.upload-name {
font-weight: 600;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.upload-status {
font-size: 12px;
color: #6c757d;
margin-top: 2px;
}
.upload-progress {
height: 8px;
background: #e9ecef;
border-radius: 999px;
overflow: hidden;
margin-top: 8px;
}
.upload-bar {
height: 100%;
width: 0%;
background: #0d6efd;
transition: width .12s linear;
}
.upload-actions {
display: flex;
align-items: center;
gap: 8px;
}
.upload-badge {
font-size: 12px;
padding: 4px 8px;
border-radius: 999px;
background: #f1f3f5;
}
.upload-badge.ok {
background: #e9f7ef;
color: #198754;
}
.upload-badge.err {
background: #fdecea;
color: #dc3545;
}
</style> </style>
</head> </head>
@ -634,12 +728,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
<h5>Foto della scuola (max 5)</h5> <h5>Foto della scuola (max 5)</h5>
<div id="photos-dropzone" style="border: 2px dashed #adb5bd; border-radius: 10px; padding: 30px; text-align: center; background: #f8f9fa; min-height: 160px; cursor: pointer;"> <div id="photos-dropzone" style="border: 2px dashed #adb5bd; border-radius: 10px; padding: 30px; text-align: center; background: #f8f9fa; min-height: 160px; cursor: pointer;">
<p class="mb-2">Trascina le immagini qui oppure</p> <p class="mb-2 dz-hint">Trascina le immagini qui oppure</p>
<button type="button" class="btn btn-outline-primary btn-sm" id="btn-select-photos">Seleziona file</button> <button type="button" class="btn btn-outline-primary btn-sm" id="btn-select-photos">Seleziona file</button>
<input type="file" id="photos-input" name="photos[]" multiple accept="image/jpeg,image/png,image/gif" style="display:none;"> <input type="file" id="photos-input" name="photos[]" multiple accept="image/jpeg,image/png,image/gif" style="display:none;">
<div class="mt-3 text-muted">Foto rimanenti: <strong id="photos-remaining"><?php echo 5 - count($existingPhotos ?? []); ?></strong></div> <div class="mt-3 text-muted">Foto rimanenti: <strong id="photos-remaining"><?php echo 5 - count($existingPhotos ?? []); ?></strong></div>
</div> </div>
<!-- Coda upload: preview + progress -->
<div id="upload-queue" class="mt-3"></div>
<div id="photos-preview" class="mt-4 d-flex flex-wrap gap-3"> <div id="photos-preview" class="mt-4 d-flex flex-wrap gap-3">
<?php foreach ($existingPhotos ?? [] as $photo): ?> <?php foreach ($existingPhotos ?? [] as $photo): ?>
<div class="position-relative" style="width:160px; height:160px;" data-photo-id="<?php echo $photo['id']; ?>"> <div class="position-relative" style="width:160px; height:160px;" data-photo-id="<?php echo $photo['id']; ?>">
@ -858,6 +955,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
const btnSelect = document.getElementById('btn-select-photos'); const btnSelect = document.getElementById('btn-select-photos');
const previewContainer = document.getElementById('photos-preview'); const previewContainer = document.getElementById('photos-preview');
const remainingSpan = document.getElementById('photos-remaining'); const remainingSpan = document.getElementById('photos-remaining');
const uploadQueue = document.getElementById('upload-queue');
let remainingPhotos = parseInt(remainingSpan?.innerText || '0'); let remainingPhotos = parseInt(remainingSpan?.innerText || '0');
@ -933,14 +1032,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
['dragover', 'dragenter'].forEach(ev => dropzone.addEventListener(ev, e => { ['dragover', 'dragenter'].forEach(ev => dropzone.addEventListener(ev, e => {
e.preventDefault(); e.preventDefault();
dropzone.classList.add('bg-light', 'border-primary'); dropzone.classList.add('is-dragover');
})); }));
['dragleave', 'drop'].forEach(ev => dropzone.addEventListener(ev, e => { ['dragleave', 'drop'].forEach(ev => dropzone.addEventListener(ev, e => {
e.preventDefault(); e.preventDefault();
dropzone.classList.remove('bg-light', 'border-primary'); dropzone.classList.remove('is-dragover');
})); }));
dropzone.addEventListener('drop', e => handleFiles(e.dataTransfer.files)); dropzone.addEventListener('drop', e => handleFiles(e.dataTransfer.files));
fileInput.addEventListener('change', e => { fileInput.addEventListener('change', e => {
@ -949,45 +1049,154 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
}); });
} }
function handleFiles(files) { function formatBytes(bytes) {
if (!bytes && bytes !== 0) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), sizes.length - 1);
return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + sizes[i];
}
function createUploadItem(file) {
const item = document.createElement('div');
item.className = 'upload-item';
item.innerHTML = `
<img class="upload-thumb" alt="preview">
<div class="upload-meta">
<div class="upload-name"></div>
<div class="upload-status">In attesa…</div>
<div class="upload-progress"><div class="upload-bar"></div></div>
</div>
<div class="upload-actions">
<span class="upload-badge">0%</span>
</div>
`;
const img = item.querySelector('.upload-thumb');
const nameEl = item.querySelector('.upload-name');
const statusEl = item.querySelector('.upload-status');
const bar = item.querySelector('.upload-bar');
const badge = item.querySelector('.upload-badge');
nameEl.textContent = file.name;
// Preview immediata
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
};
reader.readAsDataURL(file);
return {
item,
statusEl,
bar,
badge
};
}
function uploadOneFileXHR(file, ui) {
return new Promise((resolve) => {
const formData = new FormData();
formData.append('action', 'upload_photos');
// invio 1 file alla volta per avere progress per file
formData.append('photos[]', file);
const xhr = new XMLHttpRequest();
xhr.open('POST', '', true);
ui.statusEl.textContent = `Caricamento… (${formatBytes(file.size)})`;
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const pct = Math.round((e.loaded / e.total) * 100);
ui.bar.style.width = pct + '%';
ui.badge.textContent = pct + '%';
}
};
xhr.onload = () => {
let data = null;
try {
data = JSON.parse(xhr.responseText);
} catch (e) {}
if (xhr.status >= 200 && xhr.status < 300 && data && data.success && data.uploaded && data.uploaded.length) {
ui.statusEl.textContent = 'Caricato';
ui.badge.textContent = 'OK';
ui.badge.classList.add('ok');
// aggiungo anteprima definitiva nella griglia (quella sotto)
data.uploaded.forEach(item => addPhotoPreview(item.filename, item.id));
resolve({
ok: true,
uploadedCount: data.uploaded.length,
errors: data.errors || []
});
} else {
const msg = (data && data.error) ? data.error : 'Errore upload';
ui.statusEl.textContent = msg;
ui.badge.textContent = 'ERR';
ui.badge.classList.add('err');
resolve({
ok: false,
uploadedCount: 0,
errors: (data && data.errors) ? data.errors : [msg]
});
}
};
xhr.onerror = () => {
ui.statusEl.textContent = 'Errore di connessione';
ui.badge.textContent = 'ERR';
ui.badge.classList.add('err');
resolve({
ok: false,
uploadedCount: 0,
errors: ['Errore di connessione']
});
};
xhr.send(formData);
});
}
async function handleFiles(files) {
if (remainingPhotos <= 0) return alert('Limite di 5 foto raggiunto'); if (remainingPhotos <= 0) return alert('Limite di 5 foto raggiunto');
const formData = new FormData(); // filtro immagini + massimo in base a rimanenti
formData.append('action', 'upload_photos'); const selected = [];
let added = 0;
for (let file of files) { for (let file of files) {
if (added >= remainingPhotos) break; if (selected.length >= remainingPhotos) break;
if (!file.type.startsWith('image/')) continue; if (!file.type || !file.type.startsWith('image/')) continue;
formData.append('photos[]', file); selected.push(file);
added++;
} }
if (selected.length === 0) return;
if (added === 0) return; // Upload SEQUENZIALE per progress pulito e gestione limite
for (const file of selected) {
if (remainingPhotos <= 0) break;
fetch('', { const ui = createUploadItem(file);
method: 'POST', uploadQueue?.prepend(ui.item);
body: formData
}) const res = await uploadOneFileXHR(file, ui);
.then(r => r.json())
.then(data => { if (res.ok) {
if (data.success) { remainingPhotos -= res.uploadedCount;
data.uploaded.forEach(item => { updateRemainingCount();
addPhotoPreview(item.filename, item.id); }
});
remainingPhotos -= data.uploaded.length; // Se server restituisce errori, li mostro
updateRemainingCount(); if (res.errors && res.errors.length) {
} // non interrompo gli altri file, ma avviso
if (data.errors && data.errors.length) { console.warn('Upload errors:', res.errors);
alert('Problemi con alcuni file:\n' + data.errors.join('\n')); }
} }
})
.catch(err => {
console.error(err);
alert('Errore durante il caricamento');
});
} }
previewContainer?.addEventListener('click', e => { previewContainer?.addEventListener('click', e => {
// Se clicco la X: elimina // Se clicco la X: elimina
if (e.target.hasAttribute('data-delete-id')) { if (e.target.hasAttribute('data-delete-id')) {