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:
automationkriz
2026-04-05 14:48:22 +00:00
commit d296353dcb
48 changed files with 3538 additions and 0 deletions

157
static/js/app.js Normal file
View 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);