06/04 Optimize app
This commit is contained in:
@@ -591,6 +591,18 @@ textarea{resize:vertical;min-height:88px}
|
|||||||
filter:blur(4px);user-select:none;cursor:pointer;
|
filter:blur(4px);user-select:none;cursor:pointer;
|
||||||
transition:filter .2s;
|
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)
|
DYNAMIC CREDENTIAL ROWS (website admin form)
|
||||||
|
|||||||
+61
-1
@@ -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) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
document.querySelectorAll('.modal-overlay.open').forEach(m => {
|
document.querySelectorAll('.modal-overlay.open').forEach(m => {
|
||||||
m.classList.remove('open');
|
m.classList.remove('open');
|
||||||
document.body.style.overflow = '';
|
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');
|
sidebar.classList.add('open');
|
||||||
backdrop.classList.add('open');
|
backdrop.classList.add('open');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
toggle.setAttribute('aria-expanded', 'true');
|
||||||
|
toggle.setAttribute('aria-label', 'Close navigation');
|
||||||
}
|
}
|
||||||
function closeSidebar() {
|
function closeSidebar() {
|
||||||
sidebar.classList.remove('open');
|
sidebar.classList.remove('open');
|
||||||
backdrop.classList.remove('open');
|
backdrop.classList.remove('open');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
|
toggle.setAttribute('aria-expanded', 'false');
|
||||||
|
toggle.setAttribute('aria-label', 'Open navigation');
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle.addEventListener('click', openSidebar);
|
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) ───────── */
|
/* ── Generic fetch-based form submit (JSON response) ───────── */
|
||||||
async function submitJson(url, data, method = 'POST') {
|
async function submitJson(url, data, method = 'POST') {
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
|
|||||||
@@ -184,11 +184,11 @@ function addCredRow(btn) {
|
|||||||
btn.closest('.modal-body').querySelector('.cred-rows').insertAdjacentHTML('beforeend', emptyCredRow());
|
btn.closest('.modal-body').querySelector('.cred-rows').insertAdjacentHTML('beforeend', emptyCredRow());
|
||||||
}
|
}
|
||||||
function removeRow(btn) { btn.closest('.cred-row').remove(); }
|
function removeRow(btn) { btn.closest('.cred-row').remove(); }
|
||||||
function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<'); }
|
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
var searchEl = document.getElementById('site-search');
|
var searchEl = document.getElementById('site-search');
|
||||||
var typeEl = document.getElementById('site-type-filter');
|
var typeEl = document.getElementById('site-type-filter');
|
||||||
|
var _timer = null;
|
||||||
function filterSites() {
|
function filterSites() {
|
||||||
var q = searchEl.value.trim().toLowerCase();
|
var q = searchEl.value.trim().toLowerCase();
|
||||||
var type = typeEl.value;
|
var type = typeEl.value;
|
||||||
@@ -198,7 +198,10 @@ function esc(s) { return (s||'').replace(/&/g,'&').replace(/"/g,'"').re
|
|||||||
row.style.display = (nameMatch && typeMatch) ? '' : 'none';
|
row.style.display = (nameMatch && typeMatch) ? '' : 'none';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
searchEl.addEventListener('input', filterSites);
|
searchEl.addEventListener('input', function() {
|
||||||
|
clearTimeout(_timer);
|
||||||
|
_timer = setTimeout(filterSites, 250);
|
||||||
|
});
|
||||||
typeEl.addEventListener('change', filterSites);
|
typeEl.addEventListener('change', filterSites);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+3
-2
@@ -10,11 +10,12 @@
|
|||||||
<body class="layout">
|
<body class="layout">
|
||||||
|
|
||||||
<!-- Mobile sidebar toggle (hidden on desktop via CSS) -->
|
<!-- Mobile sidebar toggle (hidden on desktop via CSS) -->
|
||||||
<button class="sidebar-toggle" id="sidebar-toggle" aria-label="Open navigation">☰</button>
|
<button class="sidebar-toggle" id="sidebar-toggle"
|
||||||
|
aria-label="Open navigation" aria-expanded="false" aria-controls="sidebar">☰</button>
|
||||||
<div class="sidebar-backdrop" id="sidebar-backdrop"></div>
|
<div class="sidebar-backdrop" id="sidebar-backdrop"></div>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="sidebar" id="sidebar">
|
<aside class="sidebar" id="sidebar" aria-label="Main navigation">
|
||||||
<div class="sidebar-brand">
|
<div class="sidebar-brand">
|
||||||
<span class="brand-icon">🌐</span>
|
<span class="brand-icon">🌐</span>
|
||||||
<span class="brand-name">Website<br>Checker</span>
|
<span class="brand-name">Website<br>Checker</span>
|
||||||
|
|||||||
@@ -472,13 +472,6 @@ document.getElementById('bid-search').addEventListener('input', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── Escape ─────────────────────────────────────────────── */
|
|
||||||
function esc(str) {
|
|
||||||
return String(str || '').replace(/[&<>"']/g, function(c) {
|
|
||||||
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Init ───────────────────────────────────────────────── */
|
/* ── Init ───────────────────────────────────────────────── */
|
||||||
loadBids();
|
loadBids();
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">New Password</label>
|
<label class="form-label">New Password</label>
|
||||||
<input class="form-control" type="password" name="new_password" required>
|
<input class="form-control" type="password" name="new_password" id="new-password" required>
|
||||||
|
<div class="pw-strength-wrap">
|
||||||
|
<div class="pw-strength-bar"><div class="pw-strength-fill" id="pw-fill"></div></div>
|
||||||
|
<small class="pw-strength-label" id="pw-label"></small>
|
||||||
|
</div>
|
||||||
<small class="text-muted">Min 8 chars, uppercase, number, special character.</small>
|
<small class="text-muted">Min 8 chars, uppercase, number, special character.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -24,4 +28,5 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script>attachPasswordStrength('new-password', 'pw-fill', 'pw-label');</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -28,6 +28,10 @@
|
|||||||
<input type="password" id="password" name="password"
|
<input type="password" id="password" name="password"
|
||||||
autofocus required minlength="8"
|
autofocus required minlength="8"
|
||||||
placeholder="Minimum 8 characters">
|
placeholder="Minimum 8 characters">
|
||||||
|
<div class="pw-strength-wrap">
|
||||||
|
<div class="pw-strength-bar"><div class="pw-strength-fill" id="pw-fill"></div></div>
|
||||||
|
<small class="pw-strength-label" id="pw-label"></small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="confirm_password">Confirm Password</label>
|
<label for="confirm_password">Confirm Password</label>
|
||||||
@@ -42,5 +46,23 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var input = document.getElementById('password');
|
||||||
|
var fill = document.getElementById('pw-fill');
|
||||||
|
var lbl = document.getElementById('pw-label');
|
||||||
|
var map = ['pw-weak','pw-weak','pw-fair','pw-strong','pw-great','pw-great'];
|
||||||
|
var lbls = ['Weak','Weak','Fair','Strong','Very strong','Very strong'];
|
||||||
|
input.addEventListener('input', function() {
|
||||||
|
if (!this.value) { fill.className='pw-strength-fill'; lbl.className='pw-strength-label'; lbl.textContent=''; return; }
|
||||||
|
var pw=this.value, s=0;
|
||||||
|
if (pw.length>=8) s++; if (pw.length>=12) s++;
|
||||||
|
if (/[A-Z]/.test(pw)) s++; if (/[0-9]/.test(pw)) s++; if (/[^A-Za-z0-9]/.test(pw)) s++;
|
||||||
|
fill.className='pw-strength-fill '+map[s];
|
||||||
|
lbl.className='pw-strength-label '+map[s];
|
||||||
|
lbl.textContent=lbls[s];
|
||||||
|
});
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -345,9 +345,22 @@ document.getElementById('site-list').addEventListener('click', function(e) {
|
|||||||
|
|
||||||
/* Undo */
|
/* Undo */
|
||||||
if (btn.classList.contains('js-undo')) {
|
if (btn.classList.contains('js-undo')) {
|
||||||
if (confirm('Remove check for ' + btn.dataset.name + '?')) {
|
if (!confirm('Remove check for ' + btn.dataset.name + '?')) return;
|
||||||
btn.closest('form').submit();
|
var siteName = btn.dataset.name;
|
||||||
}
|
var formAction = btn.closest('form').action;
|
||||||
|
btn.disabled = true;
|
||||||
|
fetch(formAction, {
|
||||||
|
method: 'POST', credentials: 'same-origin',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrfToken()},
|
||||||
|
body: ''
|
||||||
|
}).then(function() {
|
||||||
|
var toast = document.createElement('div');
|
||||||
|
toast.className = 'alert alert-info';
|
||||||
|
toast.style.cssText = 'position:fixed;top:1rem;right:1rem;z-index:9999;min-width:220px;box-shadow:var(--shadow)';
|
||||||
|
toast.textContent = '↩ Check removed for ' + siteName;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
setTimeout(function() { location.reload(); }, 700);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -371,13 +384,6 @@ document.getElementById('modal-creds').addEventListener('click', function(e) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── HTML escape ───────────────────────────────────────────── */
|
|
||||||
function esc(str) {
|
|
||||||
return String(str || '').replace(/[&<>"']/g, function(c) {
|
|
||||||
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Health dots — batched with max 4 concurrent requests ─── */
|
/* ── Health dots — batched with max 4 concurrent requests ─── */
|
||||||
(function() {
|
(function() {
|
||||||
var cards = Array.from(document.querySelectorAll('.site-card'));
|
var cards = Array.from(document.querySelectorAll('.site-card'));
|
||||||
|
|||||||
Reference in New Issue
Block a user