@@ -858,6 +955,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
const btnSelect = document.getElementById('btn-select-photos');
const previewContainer = document.getElementById('photos-preview');
const remainingSpan = document.getElementById('photos-remaining');
+ const uploadQueue = document.getElementById('upload-queue');
+
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 => {
e.preventDefault();
- dropzone.classList.add('bg-light', 'border-primary');
+ dropzone.classList.add('is-dragover');
}));
['dragleave', 'drop'].forEach(ev => dropzone.addEventListener(ev, e => {
e.preventDefault();
- dropzone.classList.remove('bg-light', 'border-primary');
+ dropzone.classList.remove('is-dragover');
}));
+
dropzone.addEventListener('drop', e => handleFiles(e.dataTransfer.files));
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 = `
+
![preview]()
+
+
+ 0%
+
+ `;
+
+ 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');
- const formData = new FormData();
- formData.append('action', 'upload_photos');
-
- let added = 0;
+ // filtro immagini + massimo in base a rimanenti
+ const selected = [];
for (let file of files) {
- if (added >= remainingPhotos) break;
- if (!file.type.startsWith('image/')) continue;
- formData.append('photos[]', file);
- added++;
+ if (selected.length >= remainingPhotos) break;
+ if (!file.type || !file.type.startsWith('image/')) continue;
+ selected.push(file);
}
+ 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('', {
- method: 'POST',
- body: formData
- })
- .then(r => r.json())
- .then(data => {
- if (data.success) {
- data.uploaded.forEach(item => {
- addPhotoPreview(item.filename, item.id);
- });
- remainingPhotos -= data.uploaded.length;
- updateRemainingCount();
- }
- if (data.errors && data.errors.length) {
- alert('Problemi con alcuni file:\n' + data.errors.join('\n'));
- }
- })
- .catch(err => {
- console.error(err);
- alert('Errore durante il caricamento');
- });
+ const ui = createUploadItem(file);
+ uploadQueue?.prepend(ui.item);
+
+ const res = await uploadOneFileXHR(file, ui);
+
+ if (res.ok) {
+ remainingPhotos -= res.uploadedCount;
+ updateRemainingCount();
+ }
+
+ // Se server restituisce errori, li mostro
+ if (res.errors && res.errors.length) {
+ // non interrompo gli altri file, ma avviso
+ console.warn('Upload errors:', res.errors);
+ }
+ }
}
+
previewContainer?.addEventListener('click', e => {
// Se clicco la X: elimina
if (e.target.hasAttribute('data-delete-id')) {