Initial commit: Diario Conversazioni Olimpic Nastri
- Django 5.2 + PostgreSQL + Gunicorn - Conversazioni, Obiettivi, Documenti PDF, Persone - Commenti e aggiornamenti con modifica/eliminazione - Agenda, ricerca live, giorni rimanenti scadenze - Bootstrap 5 + HTMX + toast notifications - Deploy: Nginx + Gunicorn + SSL
This commit is contained in:
157
static/js/app.js
Normal file
157
static/js/app.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/* ── Olimpic Nastri — App JS v2 ─────────────────────────────────────────── */
|
||||
|
||||
// ── CSRF helper ──
|
||||
function getCsrf() {
|
||||
return document.cookie.split('; ')
|
||||
.find(r => r.startsWith('csrftoken='))
|
||||
?.split('=')[1] ?? '';
|
||||
}
|
||||
|
||||
// ── Toast notifications ──
|
||||
function showToast(message, type = 'success') {
|
||||
const container = document.getElementById('toast-container');
|
||||
if (!container) return;
|
||||
|
||||
const icons = {
|
||||
success: 'bi-check-circle-fill',
|
||||
danger: 'bi-exclamation-triangle-fill',
|
||||
info: 'bi-info-circle-fill',
|
||||
};
|
||||
const colors = {
|
||||
success: '#15803d',
|
||||
danger: '#dc2626',
|
||||
info: '#4361ee',
|
||||
};
|
||||
|
||||
const id = 'toast-' + Date.now();
|
||||
const html = `
|
||||
<div id="${id}" class="toast align-items-center text-white border-0 mb-2" role="alert"
|
||||
style="background:${colors[type] || colors.success};">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body d-flex align-items-center gap-2">
|
||||
<i class="bi ${icons[type] || icons.success}"></i>
|
||||
${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||
</div>
|
||||
</div>`;
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
const toastEl = document.getElementById(id);
|
||||
const bsToast = new bootstrap.Toast(toastEl, { delay: 3000 });
|
||||
bsToast.show();
|
||||
toastEl.addEventListener('hidden.bs.toast', () => toastEl.remove());
|
||||
}
|
||||
|
||||
// ── Convert Django messages to toasts on page load ──
|
||||
function convertMessagesToToasts() {
|
||||
document.querySelectorAll('[data-toast-message]').forEach(el => {
|
||||
showToast(el.dataset.toastMessage, el.dataset.toastType || 'success');
|
||||
el.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Progress slider AJAX ──
|
||||
function initSliders() {
|
||||
document.querySelectorAll('.progress-slider').forEach(slider => {
|
||||
if (slider._sliderInit) return;
|
||||
slider._sliderInit = true;
|
||||
|
||||
const id = slider.dataset.id;
|
||||
const lbl = document.getElementById('lbl-' + id);
|
||||
const saving = document.getElementById('saving-' + id);
|
||||
let timer = null;
|
||||
|
||||
slider.addEventListener('input', () => {
|
||||
const v = slider.value;
|
||||
if (lbl) lbl.textContent = v + '%';
|
||||
slider.style.setProperty('--val', v + '%');
|
||||
});
|
||||
|
||||
slider.addEventListener('change', () => {
|
||||
clearTimeout(timer);
|
||||
if (saving) {
|
||||
saving.classList.remove('d-none');
|
||||
saving.textContent = 'salvataggio…';
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
fetch(slider.dataset.url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCsrf(),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'avanzamento=' + slider.value,
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (saving) {
|
||||
saving.textContent = d.ok ? '✓ salvato' : '⚠ errore';
|
||||
setTimeout(() => saving.classList.add('d-none'), 1500);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (saving) {
|
||||
saving.textContent = '⚠ errore';
|
||||
setTimeout(() => saving.classList.add('d-none'), 1500);
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Live search dropdown ──
|
||||
function initLiveSearch() {
|
||||
const input = document.getElementById('search-input');
|
||||
const dropdown = document.getElementById('search-dropdown');
|
||||
if (!input || !dropdown) return;
|
||||
|
||||
let timer = null;
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
const q = input.value.trim();
|
||||
clearTimeout(timer);
|
||||
|
||||
if (q.length < 2) {
|
||||
dropdown.classList.remove('show');
|
||||
dropdown.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
timer = setTimeout(() => {
|
||||
fetch('/ricerca/?q=' + encodeURIComponent(q), {
|
||||
headers: { 'HX-Request': 'true' }
|
||||
})
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
dropdown.innerHTML = html;
|
||||
dropdown.classList.add('show');
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Close dropdown on click outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!input.contains(e.target) && !dropdown.contains(e.target)) {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate to full search on Enter
|
||||
input.closest('form')?.addEventListener('submit', (e) => {
|
||||
dropdown.classList.remove('show');
|
||||
});
|
||||
}
|
||||
|
||||
// ── Init everything ──
|
||||
function initApp() {
|
||||
convertMessagesToToasts();
|
||||
initSliders();
|
||||
initLiveSearch();
|
||||
}
|
||||
|
||||
// Run on initial page load
|
||||
document.addEventListener('DOMContentLoaded', initApp);
|
||||
|
||||
// Re-init after HTMX swaps
|
||||
document.addEventListener('htmx:afterSettle', initApp);
|
||||
Reference in New Issue
Block a user