diff --git a/public/userarea/school_profile.php b/public/userarea/school_profile.php index a3d06f3..c11e591 100644 --- a/public/userarea/school_profile.php +++ b/public/userarea/school_profile.php @@ -440,6 +440,100 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { transform: scale(1.03); 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; + } @@ -634,12 +728,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
Foto della scuola (max 5)
-

Trascina le immagini qui oppure

+

Trascina le immagini qui oppure

Foto rimanenti:
+ +
+
@@ -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 +
+
+
In attesa…
+
+
+
+ 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')) {