/* ── 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 = `
`;
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();
initMentions();
}
// ── @Mentions autocomplete ──
function initMentions() {
document.querySelectorAll('textarea.form-control').forEach(textarea => {
if (textarea._mentionInit) return;
textarea._mentionInit = true;
let dropdown = textarea.parentElement.querySelector('.mention-dropdown');
if (!dropdown) {
dropdown = document.createElement('div');
dropdown.className = 'mention-dropdown';
textarea.parentElement.style.position = 'relative';
textarea.parentElement.appendChild(dropdown);
}
let mentionStart = -1;
let timer = null;
textarea.addEventListener('input', () => {
const pos = textarea.selectionStart;
const text = textarea.value.substring(0, pos);
const atMatch = text.match(/@(\w[\w.]*)$/);
if (!atMatch) {
dropdown.classList.remove('show');
mentionStart = -1;
return;
}
mentionStart = pos - atMatch[0].length;
const query = atMatch[1];
clearTimeout(timer);
timer = setTimeout(() => {
fetch('/api/utenti/?q=' + encodeURIComponent(query))
.then(r => r.json())
.then(users => {
if (!users.length) {
dropdown.classList.remove('show');
return;
}
dropdown.innerHTML = users.map(u =>
`
${u.username.substring(0,2).toUpperCase()}
${u.nome}
@${u.username}
`
).join('');
dropdown.classList.add('show');
});
}, 200);
});
dropdown.addEventListener('click', (e) => {
const item = e.target.closest('.mention-item');
if (!item) return;
const username = item.dataset.username;
const before = textarea.value.substring(0, mentionStart);
const after = textarea.value.substring(textarea.selectionStart);
textarea.value = before + '@' + username + ' ' + after;
textarea.focus();
const newPos = mentionStart + username.length + 2;
textarea.setSelectionRange(newPos, newPos);
dropdown.classList.remove('show');
});
textarea.addEventListener('blur', () => {
setTimeout(() => dropdown.classList.remove('show'), 200);
});
textarea.addEventListener('keydown', (e) => {
if (!dropdown.classList.contains('show')) return;
const items = dropdown.querySelectorAll('.mention-item');
const active = dropdown.querySelector('.mention-item.active');
let idx = Array.from(items).indexOf(active);
if (e.key === 'ArrowDown') {
e.preventDefault();
if (active) active.classList.remove('active');
idx = (idx + 1) % items.length;
items[idx].classList.add('active');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (active) active.classList.remove('active');
idx = idx <= 0 ? items.length - 1 : idx - 1;
items[idx].classList.add('active');
} else if (e.key === 'Enter' && active) {
e.preventDefault();
active.click();
} else if (e.key === 'Escape') {
dropdown.classList.remove('show');
}
});
});
}
// Run on initial page load
document.addEventListener('DOMContentLoaded', initApp);
// Re-init after HTMX swaps
document.addEventListener('htmx:afterSettle', initApp);