1
0
forked from iarv/mc-webui
Files
mc-webui/app/templates/contacts.html
MarekWo 3e524bb0d2 refactor(ui): Optimize Contact Management layout for better space usage
Compact Manual Approval and Pending Contacts sections to give more
space for Existing Contacts list, which is the primary focus.

Changes to Manual Approval section:
- Convert from full section to single-line compact control
- Replace description text with tooltip icon (hover for info)
- Reduce vertical space by ~70px

Changes to Pending Contacts section:
- Reduce header size from h5 to h6
- Compact empty state (1rem padding vs 3rem)
- Reduce icon size (1.5rem vs 3rem)
- Limit list height to 200px (was 600px)
- Remove "Refresh" text from button (icon only)

Changes to Existing Contacts section:
- Dynamic height: calc(100vh - 400px) with 300px minimum
- Adapts to viewport height automatically
- On mobile: calc(100vh - 450px) for better fit
- More contacts visible without scrolling

Other improvements:
- Initialize Bootstrap tooltips in contacts.js
- Smaller fonts and margins throughout
- Better vertical space distribution

Result: ~150px more space for main contacts list on desktop,
~200px more on mobile. Tooltip provides same info without clutter.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 12:34:35 +01:00

365 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}Contact Management - mc-webui{% endblock %}
{% block extra_head %}
<style>
/* Mobile-first custom styles for Contact Management */
/* Compact manual approval section */
.compact-setting {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background-color: #f8f9fa;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
.info-icon {
color: #6c757d;
cursor: help;
font-size: 1.1rem;
}
.info-icon:hover {
color: #0d6efd;
}
.pending-contact-card {
background-color: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.contact-name {
font-size: 1.1rem;
font-weight: 600;
color: #212529;
margin-bottom: 0.5rem;
word-wrap: break-word;
}
.contact-key {
font-family: 'Courier New', monospace;
font-size: 0.85rem;
color: #6c757d;
word-break: break-all;
margin-bottom: 0.75rem;
}
.btn-action {
min-height: 44px; /* Touch-friendly size */
font-size: 1rem;
}
.empty-state {
text-align: center;
padding: 1.5rem 1rem;
color: #6c757d;
}
.empty-state i {
font-size: 2rem;
margin-bottom: 0.5rem;
opacity: 0.5;
}
.empty-state.compact {
padding: 1rem;
}
.empty-state.compact i {
font-size: 1.5rem;
margin-bottom: 0.25rem;
}
.info-badge {
display: inline-block;
background-color: #e7f3ff;
color: #0c5460;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.9rem;
margin-top: 0.5rem;
}
/* Existing Contacts Styles */
.existing-contact-card {
background-color: white;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.75rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.2s;
}
.existing-contact-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.type-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
font-weight: 600;
}
.contact-info-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.counter-badge {
font-size: 1rem;
padding: 0.35rem 0.75rem;
}
.counter-ok {
background-color: #28a745;
}
.counter-warning {
background-color: #ffc107;
color: #212529;
}
.counter-alarm {
background-color: #dc3545;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.search-toolbar {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.search-toolbar input,
.search-toolbar select {
flex: 1;
min-width: 150px;
}
/* Scrollable contacts lists */
#pendingList {
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
}
#existingList {
/* No max-height limit - let it use available space */
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
/* Dynamic height based on viewport */
max-height: calc(100vh - 400px);
min-height: 300px;
}
@media (max-width: 768px) {
#existingList {
max-height: calc(100vh - 450px);
}
}
/* Custom scrollbar styling */
#existingList::-webkit-scrollbar,
#pendingList::-webkit-scrollbar {
width: 8px;
}
#existingList::-webkit-scrollbar-track,
#pendingList::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
#existingList::-webkit-scrollbar-thumb,
#pendingList::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
#existingList::-webkit-scrollbar-thumb:hover,
#pendingList::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Compact section headers */
.section-compact {
margin-bottom: 0.75rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid px-3 py-4">
<div class="row">
<div class="col-12 col-lg-8 mx-auto">
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">
<i class="bi bi-person-check"></i> Contact Management
</h2>
<button class="btn btn-outline-secondary" onclick="window.history.back();" title="Go back">
<i class="bi bi-arrow-left"></i>
</button>
</div>
<!-- Manual Approval Settings Section (Compact) -->
<div class="compact-setting">
<div class="form-check form-switch mb-0 d-flex align-items-center gap-2">
<input class="form-check-input" type="checkbox" role="switch" id="manualApprovalSwitch" style="cursor: pointer; min-width: 3rem; min-height: 1.5rem;">
<label class="form-check-label mb-0" for="manualApprovalSwitch" style="cursor: pointer; font-weight: 500;">
<span id="switchLabel">Loading...</span>
</label>
</div>
<i class="bi bi-info-circle info-icon"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="When enabled, new contacts must be manually approved before they can communicate with your node"></i>
</div>
<!-- Pending Contacts Section (Compact) -->
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center section-compact">
<h6 class="mb-0">
<i class="bi bi-hourglass-split"></i> Pending Contacts
<span class="badge bg-primary rounded-pill" id="pendingCount" style="display: none;">0</span>
</h6>
<button class="btn btn-sm btn-outline-primary" id="refreshPendingBtn">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<!-- Loading State -->
<div id="pendingLoading" class="text-center py-2" style="display: none;">
<div class="spinner-border spinner-border-sm text-primary"></div>
<span class="ms-2 text-muted small">Loading...</span>
</div>
<!-- Empty State (Compact) -->
<div id="pendingEmpty" class="empty-state compact" style="display: none;">
<i class="bi bi-check-circle"></i>
<p class="mb-0 small">No pending requests</p>
</div>
<!-- Pending Contacts List -->
<div id="pendingList"></div>
<!-- Error State -->
<div id="pendingError" class="alert alert-danger py-2 mb-0" style="display: none;" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<span class="small" id="errorMessage">Failed to load pending contacts</span>
</div>
</div>
<!-- Existing Contacts Section -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
<i class="bi bi-person-lines-fill"></i> Existing Contacts
<span class="badge counter-badge counter-ok rounded-pill" id="contactsCounter" style="display: none;">0 / 350</span>
</h5>
<button class="btn btn-sm btn-outline-primary" id="refreshExistingBtn">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
<!-- Search and Filter Toolbar -->
<div class="search-toolbar">
<input type="text" class="form-control" id="searchInput" placeholder="Search by name or public key...">
<select class="form-select" id="typeFilter" style="max-width: 150px;">
<option value="ALL">All Types</option>
<option value="CLI">CLI</option>
<option value="REP">REP</option>
<option value="ROOM">ROOM</option>
<option value="SENS">SENS</option>
</select>
</div>
<!-- Loading State -->
<div id="existingLoading" class="text-center py-3" style="display: none;">
<div class="spinner-border spinner-border-sm text-primary"></div>
<span class="ms-2 text-muted">Loading contacts...</span>
</div>
<!-- Empty State -->
<div id="existingEmpty" class="empty-state" style="display: none;">
<i class="bi bi-inbox"></i>
<p class="mb-0">No contacts found</p>
<small class="text-muted">Contacts will appear here once added</small>
</div>
<!-- Existing Contacts List -->
<div id="existingList"></div>
<!-- Error State -->
<div id="existingError" class="alert alert-danger" style="display: none;" role="alert">
<i class="bi bi-exclamation-triangle"></i>
<span id="existingErrorMessage">Failed to load contacts</span>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteContactModal" tabindex="-1" aria-labelledby="deleteContactModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteContactModalLabel">
<i class="bi bi-exclamation-triangle"></i> Confirm Delete
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-2">Are you sure you want to delete this contact?</p>
<div class="alert alert-warning mb-0">
<strong id="deleteContactName"></strong><br>
<small class="font-monospace" id="deleteContactKey"></small>
</div>
<p class="text-muted small mt-2 mb-0">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
<i class="bi bi-trash"></i> Delete Contact
</button>
</div>
</div>
</div>
</div>
<!-- Toast container for notifications -->
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="contactToast" class="toast" role="alert">
<div class="toast-header">
<strong class="me-auto">Contact Management</strong>
<button type="button" class="btn-close" data-bs-dismiss="toast"></button>
</div>
<div class="toast-body"></div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', filename='js/contacts.js') }}"></script>
{% endblock %}