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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-01-23 08:44:55 +01:00
parent 6f1b61a488
commit a9dd31b86e
4 changed files with 102 additions and 75 deletions

View File

@@ -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<number>} 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<number>} 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 = '<i class="bi bi-check-circle"></i> 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 = '<i class="bi bi-geo-alt"></i> 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 = '<i class="bi bi-clipboard"></i> Copy Key';
copyBtn.onclick = () => copyPublicKey(contact.public_key, copyBtn);

View File

@@ -30,7 +30,7 @@
<!-- Filter and Sort Toolbar -->
<div class="filter-sort-toolbar">
<!-- Type Filter -->
<select class="form-select" id="typeFilter" style="max-width: 150px;">
<select class="form-select" id="typeFilter">
<option value="ALL">All Types</option>
<option value="CLI">CLI</option>
<option value="REP">REP</option>

View File

@@ -34,34 +34,14 @@
placeholder="Search by name or public key...">
</div>
<!-- Type Filter Checkboxes -->
<!-- Type Filter Badges -->
<div class="mb-3">
<label class="form-label small text-muted">Contact Types:</label>
<div class="d-flex flex-wrap gap-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeFilterCLI" value="CLI" checked>
<label class="form-check-label" for="typeFilterCLI">
<span class="badge bg-primary">CLI</span>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeFilterREP" value="REP">
<label class="form-check-label" for="typeFilterREP">
<span class="badge bg-success">REP</span>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeFilterROOM" value="ROOM">
<label class="form-check-label" for="typeFilterROOM">
<span class="badge bg-info">ROOM</span>
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="typeFilterSENS" value="SENS">
<label class="form-check-label" for="typeFilterSENS">
<span class="badge bg-warning text-dark">SENS</span>
</label>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="type-filter-badge active" data-type="CLI" id="typeFilterCLI">CLI</span>
<span class="type-filter-badge" data-type="REP" id="typeFilterREP">REP</span>
<span class="type-filter-badge" data-type="ROOM" id="typeFilterROOM">ROOM</span>
<span class="type-filter-badge" data-type="SENS" id="typeFilterSENS">SENS</span>
</div>
</div>

View File

@@ -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;
}
}
</style>