From a9dd31b86ec78dcc664918488a678d5b0924ba05 Mon Sep 17 00:00:00 2001 From: MarekWo Date: Fri, 23 Jan 2026 08:44:55 +0100 Subject: [PATCH] ui: Optimize Contact Management for mobile screens - Replace type filter checkboxes with clickable toggle badges in Pending Contacts (CLI/REP/ROOM/SENS) - more compact and better mobile UX - Make Pending Contacts buttons (Approve, Map, Copy Key) smaller and uniform, matching Existing Contacts button style - Optimize Existing Contacts filter toolbar to fit in one row on mobile (narrower dropdown, more compact sort buttons) Co-Authored-By: Claude Opus 4.5 --- app/static/js/contacts.js | 53 +++++++++------- app/templates/contacts-existing.html | 2 +- app/templates/contacts-pending.html | 32 ++-------- app/templates/contacts_base.html | 90 ++++++++++++++++++++-------- 4 files changed, 102 insertions(+), 75 deletions(-) diff --git a/app/static/js/contacts.js b/app/static/js/contacts.js index 278669d..c38f188 100644 --- a/app/static/js/contacts.js +++ b/app/static/js/contacts.js @@ -442,11 +442,11 @@ async function handleCleanupConfirm() { function initPendingPage() { console.log('Initializing Pending page...'); - // Load saved type filter and set checkboxes + // Load saved type filter and set badges const savedTypes = loadPendingTypeFilter(); - setTypeFilterCheckboxes(savedTypes); + setTypeFilterBadges(savedTypes); - // Load pending contacts (will use filter from checkboxes) + // Load pending contacts (will use filter from badges) loadPendingContacts(); // Attach event listeners for pending page @@ -470,11 +470,14 @@ function attachPendingEventListeners() { }); } - // Type filter checkboxes - save to localStorage and reload on change + // Type filter badges - toggle on click, save to localStorage and reload ['typeFilterCLI', 'typeFilterREP', 'typeFilterROOM', 'typeFilterSENS'].forEach(id => { - const checkbox = document.getElementById(id); - if (checkbox) { - checkbox.addEventListener('change', () => { + const badge = document.getElementById(id); + if (badge) { + badge.addEventListener('click', () => { + // Toggle active state + badge.classList.toggle('active'); + // Save selected types to localStorage const selectedTypes = getSelectedTypes(); savePendingTypeFilter(selectedTypes); @@ -688,35 +691,39 @@ function loadPendingTypeFilter() { } /** - * Set type filter checkboxes based on types array. + * Set type filter badges based on types array. * @param {Array} types - Array of contact types (1=CLI, 2=REP, 3=ROOM, 4=SENS) */ -function setTypeFilterCheckboxes(types) { - const checkboxes = { +function setTypeFilterBadges(types) { + const badges = { 1: document.getElementById('typeFilterCLI'), 2: document.getElementById('typeFilterREP'), 3: document.getElementById('typeFilterROOM'), 4: document.getElementById('typeFilterSENS') }; - // Set checkboxes based on types array - for (const [type, checkbox] of Object.entries(checkboxes)) { - if (checkbox) { - checkbox.checked = types.includes(parseInt(type)); + // Set badges based on types array + for (const [type, badge] of Object.entries(badges)) { + if (badge) { + if (types.includes(parseInt(type))) { + badge.classList.add('active'); + } else { + badge.classList.remove('active'); + } } } } /** - * Get currently selected contact types from checkboxes. + * Get currently selected contact types from badges. * @returns {Array} Array of selected types */ function getSelectedTypes() { const types = []; - if (document.getElementById('typeFilterCLI')?.checked) types.push(1); - if (document.getElementById('typeFilterREP')?.checked) types.push(2); - if (document.getElementById('typeFilterROOM')?.checked) types.push(3); - if (document.getElementById('typeFilterSENS')?.checked) types.push(4); + if (document.getElementById('typeFilterCLI')?.classList.contains('active')) types.push(1); + if (document.getElementById('typeFilterREP')?.classList.contains('active')) types.push(2); + if (document.getElementById('typeFilterROOM')?.classList.contains('active')) types.push(3); + if (document.getElementById('typeFilterSENS')?.classList.contains('active')) types.push(4); return types; } @@ -873,11 +880,11 @@ function createContactCard(contact, index) { // Action buttons const actionsDiv = document.createElement('div'); - actionsDiv.className = 'd-flex gap-2 flex-wrap mt-2'; + actionsDiv.className = 'd-flex gap-2 mt-2'; // Approve button const approveBtn = document.createElement('button'); - approveBtn.className = 'btn btn-success btn-action flex-grow-1'; + approveBtn.className = 'btn btn-sm btn-success'; approveBtn.innerHTML = ' Approve'; approveBtn.onclick = () => approveContact(contact, index); @@ -886,7 +893,7 @@ function createContactCard(contact, index) { // Map button (only if GPS coordinates available) if (contact.adv_lat && contact.adv_lon && (contact.adv_lat !== 0 || contact.adv_lon !== 0)) { const mapBtn = document.createElement('button'); - mapBtn.className = 'btn btn-outline-primary btn-action'; + mapBtn.className = 'btn btn-sm btn-outline-primary'; mapBtn.innerHTML = ' Map'; mapBtn.onclick = () => window.showContactOnMap(contact.name, contact.adv_lat, contact.adv_lon); actionsDiv.appendChild(mapBtn); @@ -894,7 +901,7 @@ function createContactCard(contact, index) { // Copy key button const copyBtn = document.createElement('button'); - copyBtn.className = 'btn btn-outline-secondary btn-action'; + copyBtn.className = 'btn btn-sm btn-outline-secondary'; copyBtn.innerHTML = ' Copy Key'; copyBtn.onclick = () => copyPublicKey(contact.public_key, copyBtn); diff --git a/app/templates/contacts-existing.html b/app/templates/contacts-existing.html index c8b4e5a..be7ad92 100644 --- a/app/templates/contacts-existing.html +++ b/app/templates/contacts-existing.html @@ -30,7 +30,7 @@
- diff --git a/app/templates/contacts-pending.html b/app/templates/contacts-pending.html index 606137b..8d967e6 100644 --- a/app/templates/contacts-pending.html +++ b/app/templates/contacts-pending.html @@ -34,34 +34,14 @@ placeholder="Search by name or public key...">
- +
-
-
- - -
-
- - -
-
- - -
-
- - -
+
+ CLI + REP + ROOM + SENS
diff --git a/app/templates/contacts_base.html b/app/templates/contacts_base.html index a7f7459..d176ca2 100644 --- a/app/templates/contacts_base.html +++ b/app/templates/contacts_base.html @@ -73,11 +73,6 @@ margin-bottom: 0.75rem; } - .btn-action { - min-height: 44px; /* Touch-friendly size */ - font-size: 1rem; - } - .empty-state { text-align: center; padding: 1.5rem 1rem; @@ -130,6 +125,58 @@ font-weight: 600; } + /* Clickable type filter badges */ + .type-filter-badge { + display: inline-block; + padding: 0.25rem 0.6rem; + font-size: 0.8rem; + font-weight: 600; + border-radius: 0.375rem; + cursor: pointer; + transition: all 0.15s ease-in-out; + user-select: none; + } + + .type-filter-badge[data-type="CLI"] { + color: #0d6efd; + background-color: white; + border: 2px solid #0d6efd; + } + .type-filter-badge[data-type="CLI"].active { + color: white; + background-color: #0d6efd; + } + + .type-filter-badge[data-type="REP"] { + color: #198754; + background-color: white; + border: 2px solid #198754; + } + .type-filter-badge[data-type="REP"].active { + color: white; + background-color: #198754; + } + + .type-filter-badge[data-type="ROOM"] { + color: #0dcaf0; + background-color: white; + border: 2px solid #0dcaf0; + } + .type-filter-badge[data-type="ROOM"].active { + color: white; + background-color: #0dcaf0; + } + + .type-filter-badge[data-type="SENS"] { + color: #ffc107; + background-color: white; + border: 2px solid #ffc107; + } + .type-filter-badge[data-type="SENS"].active { + color: #212529; + background-color: #ffc107; + } + .contact-info-row { display: flex; align-items: center; @@ -251,31 +298,37 @@ font-weight: 600; } - /* NEW: Sort toolbar */ + /* Sort toolbar */ .filter-sort-toolbar { display: flex; - gap: 0.5rem; + gap: 0.375rem; margin-bottom: 1rem; align-items: center; - flex-wrap: wrap; + } + + .filter-sort-toolbar .form-select { + width: auto; + padding: 0.35rem 2rem 0.35rem 0.5rem; + font-size: 0.85rem; } .sort-buttons { display: flex; - gap: 0.5rem; + gap: 0.375rem; } .sort-btn { display: flex; align-items: center; - gap: 0.25rem; - padding: 0.5rem 1rem; + gap: 0.2rem; + padding: 0.35rem 0.5rem; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 0.375rem; cursor: pointer; - font-size: 0.9rem; + font-size: 0.85rem; transition: all 0.2s; + white-space: nowrap; } .sort-btn:hover { @@ -344,19 +397,6 @@ .contacts-list-fullscreen { height: calc(100vh - 300px); } - - .filter-sort-toolbar { - flex-direction: column; - align-items: stretch; - } - - .sort-buttons { - width: 100%; - } - - .sort-btn { - flex: 1; - } }