fixed photo upload
This commit is contained in:
parent
2734679938
commit
e4b472f0c1
@ -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')) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user