From e03eb26417966814c1bcaf2bb83f1fed3f0d4f8f Mon Sep 17 00:00:00 2001 From: NguyenND Date: Thu, 4 Jun 2026 15:00:09 -0400 Subject: [PATCH] 06/04 Optimize app --- static/css/style.css | 12 +++++++ static/js/app.js | 62 +++++++++++++++++++++++++++++++++- templates/admin/websites.html | 7 ++-- templates/base.html | 5 +-- templates/bid_tracker.html | 7 ---- templates/change_password.html | 7 +++- templates/reset_password.html | 22 ++++++++++++ templates/user/dashboard.html | 26 ++++++++------ 8 files changed, 125 insertions(+), 23 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index d75838f..3b1e9a7 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -591,6 +591,18 @@ textarea{resize:vertical;min-height:88px} filter:blur(4px);user-select:none;cursor:pointer; transition:filter .2s; } +.pw-strength-wrap { margin-top:.4rem; } +.pw-strength-bar { height:4px;border-radius:2px;background:var(--border);overflow:hidden;margin-bottom:.3rem; } +.pw-strength-fill { height:100%;border-radius:2px;width:0%;transition:width .3s,background .3s; } +.pw-strength-fill.pw-weak { width:25%;background:var(--danger); } +.pw-strength-fill.pw-fair { width:50%;background:var(--warning); } +.pw-strength-fill.pw-strong { width:75%;background:var(--accent); } +.pw-strength-fill.pw-great { width:100%;background:var(--success); } +.pw-strength-label { font-size:.75rem; } +.pw-strength-label.pw-weak { color:var(--danger); } +.pw-strength-label.pw-fair { color:var(--warning); } +.pw-strength-label.pw-strong { color:var(--accent); } +.pw-strength-label.pw-great { color:var(--success); } /* ══════════════════════════════════════════════════════════ DYNAMIC CREDENTIAL ROWS (website admin form) diff --git a/static/js/app.js b/static/js/app.js index 515a039..e6a97c3 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -32,13 +32,30 @@ document.addEventListener('click', (e) => { } }); -// Close modal on Escape key +// 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(); } + } } }); @@ -255,11 +272,15 @@ function getCsrfToken() { 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); @@ -290,6 +311,45 @@ document.addEventListener('DOMContentLoaded', function() { }); }); +/* ── Password strength meter ───────────────────────────────── */ +function _pwStrengthLevel(pw) { + var score = 0; + if (pw.length >= 8) score++; + if (pw.length >= 12) score++; + if (/[A-Z]/.test(pw)) score++; + if (/[0-9]/.test(pw)) score++; + if (/[^A-Za-z0-9]/.test(pw)) score++; + var map = ['pw-weak','pw-weak','pw-fair','pw-strong','pw-great','pw-great']; + var lbl = ['Weak','Weak','Fair','Strong','Very strong','Very strong']; + return { cls: map[score], label: lbl[score] }; +} + +function attachPasswordStrength(inputId, fillId, labelId) { + var input = document.getElementById(inputId); + var fill = document.getElementById(fillId); + var lbl = document.getElementById(labelId); + if (!input || !fill || !lbl) return; + input.addEventListener('input', function() { + if (!this.value) { + fill.className = 'pw-strength-fill'; + lbl.className = 'pw-strength-label'; + lbl.textContent = ''; + return; + } + var r = _pwStrengthLevel(this.value); + fill.className = 'pw-strength-fill ' + r.cls; + lbl.className = 'pw-strength-label ' + r.cls; + lbl.textContent = r.label; + }); +} + +/* ── 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, { diff --git a/templates/admin/websites.html b/templates/admin/websites.html index b8e41d0..3348ce3 100644 --- a/templates/admin/websites.html +++ b/templates/admin/websites.html @@ -184,11 +184,11 @@ function addCredRow(btn) { btn.closest('.modal-body').querySelector('.cred-rows').insertAdjacentHTML('beforeend', emptyCredRow()); } function removeRow(btn) { btn.closest('.cred-row').remove(); } -function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/ diff --git a/templates/base.html b/templates/base.html index faaf48d..c0bebce 100644 --- a/templates/base.html +++ b/templates/base.html @@ -10,11 +10,12 @@ - + -