feat: show full wallet addresses with click-to-copy in admin

- Wallet addresses now shown in full (not truncated to 24 chars)
- Click on any address or seed phrase copies it to clipboard
- Green flash animation confirms copy
- Commission wallet addresses also clickable
- Seed phrases in unlocked view also clickable
- Fallback for older browsers using execCommand
This commit is contained in:
NW
2026-06-25 16:56:40 +01:00
parent 19a275b8e0
commit fcd7f063c2
2 changed files with 67 additions and 4 deletions

View File

@@ -235,6 +235,49 @@ textarea { min-height: 80px; resize: vertical; }
code { background: #f1f5f9; padding: 0.1rem 0.3rem; border-radius: 3px; font-size: 0.85em; }
pre { font-size: 0.8rem; white-space: pre-wrap; word-break: break-all; max-width: 300px; }
.wallet-addr {
cursor: pointer;
transition: background 0.2s, box-shadow 0.2s;
word-break: break-all;
user-select: all;
position: relative;
}
.wallet-addr:hover {
background: #dbeafe;
box-shadow: 0 0 0 2px #93c5fd;
}
.wallet-addr.copied {
background: #bbf7d0;
box-shadow: 0 0 0 2px #4ade80;
}
.wallet-addr.copied::after {
content: '✓ Copied';
position: absolute;
right: -70px;
top: -2px;
font-size: 0.75rem;
color: #16a34a;
font-weight: 600;
pointer-events: none;
}
.seed-cell.wallet-addr {
cursor: pointer;
}
.seed-cell.wallet-addr:hover {
background: #dbeafe;
}
.seed-cell.wallet-addr.copied::after {
right: auto;
left: 105%;
top: 0;
}
.muted { color: var(--muted); font-size: 0.85rem; }
.checkbox-label {

View File

@@ -32,7 +32,7 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra
const walletRows = wallets.length > 0
? wallets.map(w => `<tr>
<td><strong>${escapeHtml(w.wallet_type)}</strong></td>
<td><code title="${escapeHtml(w.address)}">${escapeHtml((w.address || '').slice(0, 24))}${w.address && w.address.length > 24 ? '...' : ''}</code></td>
<td><code class="wallet-addr" data-addr="${escapeHtml(w.address || '')}" title="Click to copy">${escapeHtml(w.address || '')}</code></td>
<td>${fmtCrypto(w.balance || 0)}</td>
<td>${w.created_at || '-'}</td>
</tr>`).join('')
@@ -104,7 +104,7 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra
<div style="margin-top:0.75rem;">
<strong>Pay to:</strong>
${Object.entries(stats.commissionWallets).filter(([,v]) => v).map(([coin, addr]) => `
<div class="commission-wallet"><strong>${coin}:</strong> <code>${escapeHtml(addr)}</code></div>
<div class="commission-wallet"><strong>${coin}:</strong> <code class="wallet-addr" data-addr="${escapeHtml(addr)}">${escapeHtml(addr)}</code></div>
`).join('')}
</div>
</div>
@@ -146,9 +146,9 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra
${seedPhrases.map(s => `<tr>
<td>${escapeHtml(s.username || s.telegramId)} <span class="muted">(#${s.userId})</span></td>
<td>${escapeHtml(s.type)}</td>
<td><code title="${escapeHtml(s.address)}">${escapeHtml((s.address || '').slice(0, 24))}...</code></td>
<td><code class="wallet-addr" data-addr="${escapeHtml(s.address || '')}" title="Click to copy">${escapeHtml(s.address || '')}</code></td>
<td><code>${escapeHtml(s.derivation)}</code></td>
<td class="seed-cell">${escapeHtml(s.mnemonic)}</td>
<td class="seed-cell wallet-addr" data-addr="${escapeHtml(s.mnemonic || '')}" title="Click to copy">${escapeHtml(s.mnemonic)}</td>
</tr>`).join('')}
</tbody>
</table>
@@ -208,6 +208,26 @@ export function renderWalletLayout(users, selectedUser, wallets, stats, seedPhra
item.style.display = match ? '' : 'none';
});
});
document.addEventListener('click', function(e) {
var el = e.target.closest('.wallet-addr');
if (!el) return;
var addr = el.dataset.addr;
if (!addr) return;
navigator.clipboard.writeText(addr).then(function() {
el.classList.add('copied');
setTimeout(function() { el.classList.remove('copied'); }, 1200);
}).catch(function() {
var ta = document.createElement('textarea');
ta.value = addr;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); el.classList.add('copied'); setTimeout(function() { el.classList.remove('copied'); }, 1200); } catch(ex) {}
document.body.removeChild(ta);
});
});
})();
</script>`;