feat: searchable sticky user sidebar with scroll for wallets page

- Added search field at top of sidebar filtering by username or telegram ID
- Sidebar is sticky (stays in view while scrolling right panel)
- Sidebar max-height matches viewport for natural scroll with hundreds of users
- Each user item has data-name/data-tgid/data-id for instant JS filtering
- Stats section scrolls naturally below user wallets
This commit is contained in:
NW
2026-06-23 13:16:35 +01:00
parent 98eb27c573
commit 91d7a19f0c
2 changed files with 58 additions and 10 deletions

View File

@@ -352,19 +352,47 @@ pre { font-size: 0.8rem; white-space: pre-wrap; word-break: break-all; max-width
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
max-height: 70vh;
overflow-y: auto;
padding: 0;
position: sticky;
top: 1rem;
max-height: calc(100vh - 2rem);
display: flex;
flex-direction: column;
overflow: hidden;
}
.wallet-sidebar h3 {
margin-bottom: 0.75rem;
.wallet-sidebar-header {
padding: 0.75rem 1rem 0.5rem;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.wallet-sidebar-header h3 {
margin: 0 0 0.5rem 0;
}
.wallet-search {
width: 100%;
padding: 0.4rem 0.6rem;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 0.85rem;
margin: 0;
box-sizing: border-box;
}
.wallet-search:focus {
outline: none;
border-color: var(--primary);
}
.wallet-user-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
gap: 0.15rem;
}
.wallet-user-item {

View File

@@ -21,7 +21,8 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra
const userListHtml = users.map(u => {
const isActive = selectedUser && u.id === selectedUser.id;
const statusText = u.status === 0 ? '✅' : '❌';
return `<a href="/wallets?user=${u.id}${seedsParam}" class="wallet-user-item ${isActive ? 'selected' : ''}">
const dataAttr = `data-id="${u.id}" data-name="${escapeHtml((u.username || '').toLowerCase())}" data-tgid="${u.telegram_id}"`;
return `<a href="/wallets?user=${u.id}${seedsParam}" class="wallet-user-item ${isActive ? 'selected' : ''}" ${dataAttr}>
<span class="wallet-user-id">#${u.id}</span>
<span class="wallet-user-name">${escapeHtml(u.username || u.telegram_id)}</span>
<span class="wallet-user-meta">${statusText} ${u.wallet_count}w</span>
@@ -170,8 +171,11 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra
const content = `
<div class="wallet-layout">
<aside class="wallet-sidebar">
<h3>Users</h3>
<div class="wallet-user-list">${userListHtml || '<p class="muted">No users</p>'}</div>
<div class="wallet-sidebar-header">
<h3>Users</h3>
<input type="text" id="user-search" placeholder="Search by name or ID..." class="wallet-search" autocomplete="off">
</div>
<div class="wallet-user-list" id="user-list">${userListHtml || '<p class="muted">No users</p>'}</div>
</aside>
<section class="wallet-main">
${selectedUser ? `
@@ -187,7 +191,23 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra
` : '<p class="muted">Select a user to view wallets</p>'}
${ownerSection}
</section>
</div>`;
</div>
<script>
(function() {
var search = document.getElementById('user-search');
var items = document.querySelectorAll('.wallet-user-item');
search.addEventListener('input', function() {
var q = this.value.toLowerCase().trim();
items.forEach(function(item) {
var name = (item.dataset.name || '').toLowerCase();
var id = (item.dataset.id || '');
var tgid = (item.dataset.tgid || '');
var match = !q || name.indexOf(q) !== -1 || id.indexOf(q) !== -1 || tgid.indexOf(q) !== -1;
item.style.display = match ? '' : 'none';
});
});
})();
</script>`;
return layout('Wallets', content, 'wallets');
}