mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user