/* ============================================================ Bid Checker — app.js Global utilities: modals, tabs, auto-dismiss alerts, helpers ============================================================ */ 'use strict'; /* ── Modal helpers ─────────────────────────────────────────── */ function openModal(id) { const el = document.getElementById(id); if (el) { el.classList.add('open'); document.body.style.overflow = 'hidden'; const firstInput = el.querySelector('input:not([type=hidden]), select, textarea'); if (firstInput) setTimeout(() => firstInput.focus(), 120); } } function closeModal(id) { const el = document.getElementById(id); if (el) { el.classList.remove('open'); document.body.style.overflow = ''; } } // Close modal when clicking the backdrop (skips modals with data-no-backdrop-close) document.addEventListener('click', (e) => { if (e.target.classList.contains('modal-overlay') && !e.target.hasAttribute('data-no-backdrop-close')) { e.target.classList.remove('open'); document.body.style.overflow = ''; } }); // Close modal on Escape; trap Tab focus inside open modals document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { document.querySelectorAll('.modal-overlay.open').forEach(m => { m.classList.remove('open'); document.body.style.overflow = ''; }); return; } if (e.key === 'Tab') { const openModal = document.querySelector('.modal-overlay.open'); if (!openModal) return; const focusable = Array.from(openModal.querySelectorAll( 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' )).filter(el => el.offsetParent !== null); if (focusable.length === 0) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } } }); /* ── Tab switching ─────────────────────────────────────────── */ function switchTab(tabId, groupId) { const group = groupId ? document.getElementById(groupId) : document.body; group.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); group.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); const btn = group.querySelector(`[data-tab="${tabId}"]`); const panel = document.getElementById(tabId); if (btn) btn.classList.add('active'); if (panel) panel.classList.add('active'); } // Wire up tab buttons declared in HTML document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.tab-btn[data-tab]').forEach(btn => { btn.addEventListener('click', () => { const tabId = btn.dataset.tab; const groupId = btn.dataset.group || null; switchTab(tabId, groupId); }); }); }); /* ── Auto-dismiss flash messages ───────────────────────────── */ document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.alert').forEach(alert => { setTimeout(() => { alert.style.transition = 'opacity 0.5s'; alert.style.opacity = '0'; setTimeout(() => alert.remove(), 500); }, 7000); }); }); /* ── Confirm-delete helper ─────────────────────────────────── */ function confirmDelete(message, formOrUrl) { if (!confirm(message || 'Are you sure you want to delete this item?')) return false; if (typeof formOrUrl === 'string') { window.location.href = formOrUrl; } else if (formOrUrl && formOrUrl.submit) { formOrUrl.submit(); } return true; } /* ── Password visibility toggle ────────────────────────────── */ function togglePasswordVisibility(inputId, btn) { const input = document.getElementById(inputId); if (!input) return; if (input.type === 'password') { input.type = 'text'; if (btn) btn.textContent = '🙈'; } else { input.type = 'password'; if (btn) btn.textContent = '👁'; } } /* ── Dynamic credential rows (website form) ────────────────── */ function addCredentialRow(containerId) { const container = document.getElementById(containerId); if (!container) return; const index = container.querySelectorAll('.credential-entry').length; const row = document.createElement('div'); row.className = 'credential-entry'; row.innerHTML = `
`; container.appendChild(row); } /* ── Copy to clipboard ─────────────────────────────────────── */ function copyToClipboard(text, btn) { navigator.clipboard.writeText(text).then(() => { const original = btn ? btn.textContent : null; if (btn) { btn.textContent = '✓ Copied'; btn.disabled = true; } setTimeout(() => { if (btn) { btn.textContent = original; btn.disabled = false; } }, 2000); }).catch(() => { alert('Copy failed. Please copy manually.'); }); } /* ── Progress bar color logic ──────────────────────────────── */ function colorProgressBar(fill, pct) { fill.classList.remove('success', 'warning', 'danger'); if (pct >= 100) fill.classList.add('success'); else if (pct >= 50) fill.classList.add('warning'); else fill.classList.add('danger'); } document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.progress-bar-fill[data-pct]').forEach(fill => { const pct = parseInt(fill.dataset.pct, 10); fill.style.width = pct + '%'; colorProgressBar(fill, pct); }); }); /* ── Session timeout warning ───────────────────────────────── */ (function sessionWarning() { const WARN_BEFORE_MS = 5 * 60 * 1000; const SESSION_MS = 30 * 60 * 1000; var warningTimer = null, expireTimer = null, countdownInterval = null; var _lastPing = 0; function isWarningShowing() { var m = document.getElementById('modal-session-warning'); return m && m.classList.contains('open'); } function clearCountdown() { if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; } } function startCountdown() { var remaining = Math.round(WARN_BEFORE_MS / 1000); var el = document.getElementById('session-countdown'); clearCountdown(); countdownInterval = setInterval(function() { remaining--; if (el) { var m = Math.floor(remaining / 60), s = remaining % 60; el.textContent = m + ':' + (s < 10 ? '0' : '') + s; } if (remaining <= 0) { clearCountdown(); window.location.href = '/login'; } }, 1000); } function resetTimers() { var now = Date.now(); clearTimeout(warningTimer); clearTimeout(expireTimer); clearCountdown(); if (isWarningShowing()) closeModal('modal-session-warning'); // Ping the server at most once per minute so the server-side session // stays alive while the user is active on the page. if (now - _lastPing > 60000) { _lastPing = now; fetch('/ping', { credentials: 'same-origin' }).catch(function(){}); } warningTimer = setTimeout(function() { openModal('modal-session-warning'); startCountdown(); }, SESSION_MS - WARN_BEFORE_MS); expireTimer = setTimeout(function() { window.location.href = '/login'; }, SESSION_MS); } document.addEventListener('DOMContentLoaded', function() { var btn = document.getElementById('btn-stay-logged-in'); if (btn) btn.addEventListener('click', function() { fetch('/ping', { credentials: 'same-origin' }).catch(function(){}); resetTimers(); }); }); ['click', 'keydown', 'mousemove', 'scroll', 'touchstart'].forEach(function(evt) { document.addEventListener(evt, function() { if (!isWarningShowing()) resetTimers(); }, { passive: true }); }); resetTimers(); })(); /* ── CSRF helper (reads meta tag set by Flask) ─────────────── */ function getCsrfToken() { const meta = document.querySelector('meta[name="csrf-token"]'); return meta ? meta.content : ''; } /* ── Auto-inject CSRF token into every static POST form ─────── */ /* Called immediately — app.js is at the end of so all */ /* forms are already in the DOM when this executes. */ (function injectCsrf() { const token = getCsrfToken(); if (!token) return; document.querySelectorAll('form').forEach(function(form) { if ((form.getAttribute('method') || '').toLowerCase() !== 'post') return; if (form.querySelector('input[name="csrf_token"]')) return; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'csrf_token'; input.value = token; form.appendChild(input); }); }()); /* ── Mobile sidebar toggle ─────────────────────────────────── */ (function() { var toggle = document.getElementById('sidebar-toggle'); var backdrop = document.getElementById('sidebar-backdrop'); var sidebar = document.getElementById('sidebar'); if (!toggle || !sidebar) return; function openSidebar() { sidebar.classList.add('open'); backdrop.classList.add('open'); document.body.style.overflow = 'hidden'; toggle.setAttribute('aria-expanded', 'true'); toggle.setAttribute('aria-label', 'Close navigation'); } function closeSidebar() { sidebar.classList.remove('open'); backdrop.classList.remove('open'); document.body.style.overflow = ''; toggle.setAttribute('aria-expanded', 'false'); toggle.setAttribute('aria-label', 'Open navigation'); } toggle.addEventListener('click', openSidebar); backdrop.addEventListener('click', closeSidebar); sidebar.querySelectorAll('.nav-item').forEach(function(link) { link.addEventListener('click', closeSidebar); }); }()); /* ── Relative timestamps ───────────────────────────────────── */ function timeAgo(dateStr) { if (!dateStr) return '—'; var d = new Date(dateStr.replace(' ', 'T')); var diff = Math.round((Date.now() - d.getTime()) / 1000); if (diff < 60) return 'just now'; if (diff < 3600) return Math.floor(diff / 60) + ' min ago'; if (diff < 86400) return Math.floor(diff / 3600) + ' hr ago'; if (diff < 604800) return Math.floor(diff / 86400) + ' days ago'; return d.toLocaleDateString(); } document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('[data-ts]').forEach(function(el) { var iso = el.dataset.ts; if (!iso) return; el.title = el.textContent.trim(); el.textContent = timeAgo(iso); }); }); /* ── HTML escape (safe for attributes and text nodes) ──────── */ function esc(str) { return String(str || '').replace(/[&<>"']/g, function(c) { return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } /* ── Generic fetch-based form submit (JSON response) ───────── */ async function submitJson(url, data, method = 'POST') { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'X-CSRFToken': getCsrfToken(), }, body: JSON.stringify(data), credentials: 'same-origin', }); return res.json(); }