feat: sitewide Solana wallet connect in navbar

This commit is contained in:
jae 2026-04-06 00:05:30 +00:00
parent e854944704
commit 4fdd4aaf1c
32 changed files with 857 additions and 198 deletions

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -139,6 +139,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -75,6 +75,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -140,6 +140,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -76,6 +76,7 @@
</footer> </footer>
<script src="/js/blog.js"></script> <script src="/js/blog.js"></script>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
</body> </body>
</html> </html>

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -69,6 +69,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -328,6 +328,265 @@ a:hover { color: #fff; text-shadow: none; }
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* === NAV WALLET BUTTON === */
.nav-wallet-wrap {
position: relative;
display: flex;
align-items: center;
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.nav-wallet-btn {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.85rem;
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 1.5px;
color: var(--warning);
background: rgba(201, 162, 39, 0.08);
border: 1px solid rgba(201, 162, 39, 0.3);
cursor: pointer;
transition: all 0.3s ease;
white-space: nowrap;
}
.nav-wallet-btn:hover {
background: rgba(201, 162, 39, 0.15);
border-color: rgba(201, 162, 39, 0.5);
color: #e0c040;
}
.nav-wallet-btn.connected {
color: #14F195;
background: rgba(20, 241, 149, 0.06);
border-color: rgba(20, 241, 149, 0.25);
}
.nav-wallet-btn.connected:hover {
background: rgba(20, 241, 149, 0.12);
border-color: rgba(20, 241, 149, 0.4);
}
.nav-wallet-icon {
font-size: 0.65rem;
}
.nav-wallet-dot {
color: #14F195;
font-size: 0.55rem;
text-shadow: 0 0 6px rgba(20, 241, 149, 0.6);
}
.nav-wallet-label {
font-family: var(--font-mono);
}
/* Wallet Dropdown */
.nav-wallet-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 280px;
max-width: 340px;
background: rgba(14, 14, 14, 0.98);
border: 1px solid var(--border);
border-top: 2px solid #14F195;
backdrop-filter: blur(20px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
z-index: 1100;
padding: 0.5rem 0;
animation: nwDropIn 0.2s ease;
}
.nav-wallet-dropdown.hidden {
display: none;
}
@keyframes nwDropIn {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
/* Dropdown labels */
.nw-dd-label {
font-family: var(--font-display);
font-size: 0.6rem;
letter-spacing: 2px;
color: var(--text-muted);
padding: 0.5rem 0.85rem 0.35rem;
}
.nw-dd-label-dim {
color: #444;
}
/* Connected section */
.nw-dd-section {
padding: 0.5rem 0.85rem;
}
.nw-dd-addr {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.5rem 0.6rem;
margin: 0.35rem 0;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.2s;
}
.nw-dd-addr:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(20, 241, 149, 0.3);
}
.nw-dd-addr-text {
font-family: var(--font-mono);
font-size: 0.6rem;
color: var(--text-primary);
word-break: break-all;
line-height: 1.4;
}
.nw-dd-copy-icon {
font-size: 0.8rem;
color: var(--text-muted);
flex-shrink: 0;
}
.nw-dd-via {
font-family: var(--font-mono);
font-size: 0.55rem;
color: var(--text-muted);
letter-spacing: 1px;
}
/* Dropdown actions */
.nw-dd-actions {
border-top: 1px solid var(--border);
margin-top: 0.25rem;
}
.nw-dd-action {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.6rem 0.85rem;
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 1px;
color: var(--text-secondary);
background: none;
border: none;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.nw-dd-action:last-child {
border-bottom: none;
}
.nw-dd-action:hover {
background: rgba(255, 255, 255, 0.03);
color: #fff;
padding-left: 1rem;
}
.nw-dd-disconnect:hover {
color: var(--danger);
}
/* Wallet picker items */
.nw-dd-wallet {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
padding: 0.55rem 0.85rem;
font-family: var(--font-mono);
font-size: 0.65rem;
color: var(--text-secondary);
background: none;
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.nw-dd-wallet:hover {
background: rgba(255, 255, 255, 0.04);
color: #fff;
}
.nw-dd-wallet:last-child {
border-bottom: none;
}
.nw-dd-wallet-icon {
font-size: 1rem;
flex-shrink: 0;
width: 1.4rem;
text-align: center;
}
.nw-dd-wallet-name {
flex: 1;
letter-spacing: 1.5px;
font-weight: 500;
}
.nw-dd-wallet-tag {
font-size: 0.5rem;
letter-spacing: 1px;
color: #14F195;
flex-shrink: 0;
}
.nw-dd-wallet-install .nw-dd-wallet-tag {
color: var(--text-muted);
}
.nw-dd-wallet-install .nw-dd-wallet-name {
color: var(--text-muted);
}
.nw-dd-error {
color: var(--danger) !important;
}
.nw-dd-divider {
height: 1px;
background: var(--border);
margin: 0.35rem 0;
}
.nw-dd-empty {
padding: 1rem 0.85rem;
font-family: var(--font-mono);
font-size: 0.65rem;
color: var(--text-muted);
text-align: center;
letter-spacing: 1px;
line-height: 1.6;
}
.nw-dd-empty-hint {
font-size: 0.55rem;
color: #14F195;
}
/* === HERO SECTION === */ /* === HERO SECTION === */
/* ============================ /* ============================
HERO HUD GRID LAYOUT HERO HUD GRID LAYOUT
@ -1629,6 +1888,35 @@ a:hover { color: #fff; text-shadow: none; }
opacity: 1; opacity: 1;
} }
/* Wallet button mobile */
.nav-wallet-wrap {
position: static;
width: 100%;
padding: 0.75rem 2rem;
border-top: 1px solid var(--border);
margin: 0;
}
.nav-wallet-btn {
width: 100%;
justify-content: center;
padding: 0.6rem 1rem;
font-size: 0.75rem;
}
.nav-wallet-dropdown {
position: static;
min-width: 100%;
max-width: 100%;
border: none;
border-top: 1px solid var(--border);
box-shadow: none;
backdrop-filter: none;
background: rgba(14, 14, 14, 0.95);
animation: none;
}
/* HUD grid stacks on mobile */ /* HUD grid stacks on mobile */
.hero-hud-grid { .hero-hud-grid {

View file

@ -62,6 +62,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
<script src="/js/contraband.js"></script> <script src="/js/contraband.js"></script>

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -75,6 +75,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -44,6 +44,7 @@
<div class="footer-right"><span class="footer-signal">SIGNAL ████<span class="signal-flicker"></span></span></div> <div class="footer-right"><span class="footer-signal">SIGNAL ████<span class="signal-flicker"></span></span></div>
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -59,6 +59,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
<script src="/js/awesomelist.js?v=20260404"></script> <script src="/js/awesomelist.js?v=20260404"></script>

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -75,6 +75,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -49,6 +49,7 @@
</div> </div>
</section> </section>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
<script src="/js/changelog.js"></script> <script src="/js/changelog.js"></script>

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -582,6 +582,7 @@
</footer> </footer>
<script src="/js/main.js"></script> <script src="/js/main.js"></script>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/chat.js"></script> <script src="/js/chat.js"></script>
<script src="/js/globe.js"></script> <script src="/js/globe.js"></script>

223
js/nav.js
View file

@ -1,5 +1,6 @@
/* ─── Dynamic Navigation with Dropdown Submenus ─── */ /* ─── Dynamic Navigation with Dropdown Submenus ─── */
/* Fetches nav items from /api/navigation and renders dropdowns */ /* Fetches nav items from /api/navigation and renders dropdowns */
/* Includes sitewide wallet connect button via window.solWallet */
(function() { (function() {
'use strict'; 'use strict';
@ -84,6 +85,9 @@
// Bind mobile dropdown toggles after rendering // Bind mobile dropdown toggles after rendering
initMobileDropdowns(); initMobileDropdowns();
// Inject wallet button after nav loads
injectWalletButton();
} catch (err) { } catch (err) {
console.warn('Nav load failed, keeping existing:', err); console.warn('Nav load failed, keeping existing:', err);
} }
@ -111,7 +115,6 @@
parent.classList.add('dropdown-open'); parent.classList.add('dropdown-open');
} else { } else {
// Dropdown already open — allow navigation or close // Dropdown already open — allow navigation or close
// If tapped again on same parent link, close dropdown
e.preventDefault(); e.preventDefault();
parent.classList.remove('dropdown-open'); parent.classList.remove('dropdown-open');
} }
@ -147,6 +150,224 @@
} }
} }
// ─── Wallet Button Injection ────────────────────────────────
function injectWalletButton() {
const navContainer = document.querySelector('.nav-container');
const navStatus = document.querySelector('.nav-status');
if (!navContainer) return;
// Don't inject twice
if (document.getElementById('navWalletWrap')) return;
// Create wallet wrapper
const wrap = document.createElement('div');
wrap.id = 'navWalletWrap';
wrap.className = 'nav-wallet-wrap';
wrap.innerHTML = `
<button class="nav-wallet-btn" id="navWalletBtn">
<span class="nav-wallet-icon"></span>
<span class="nav-wallet-label">CONNECT</span>
</button>
<div class="nav-wallet-dropdown hidden" id="navWalletDropdown"></div>
`;
// Insert before nav-status (or append to container)
if (navStatus) {
navContainer.insertBefore(wrap, navStatus);
} else {
navContainer.appendChild(wrap);
}
// Bind click
const btn = document.getElementById('navWalletBtn');
btn.addEventListener('click', (e) => {
e.stopPropagation();
toggleWalletDropdown();
});
// Close dropdown on outside click
document.addEventListener('click', (e) => {
if (!e.target.closest('#navWalletWrap')) {
closeWalletDropdown();
}
});
// Listen for wallet events
window.addEventListener('wallet-connected', () => updateNavWalletUI());
window.addEventListener('wallet-disconnected', () => updateNavWalletUI());
// Set initial state
updateNavWalletUI();
}
function updateNavWalletUI() {
const btn = document.getElementById('navWalletBtn');
if (!btn) return;
const sw = window.solWallet;
if (sw && sw.connected && sw.address) {
btn.className = 'nav-wallet-btn connected';
btn.innerHTML = `
<span class="nav-wallet-dot"></span>
<span class="nav-wallet-label">${sw.truncAddr(sw.address)}</span>
`;
} else {
btn.className = 'nav-wallet-btn';
btn.innerHTML = `
<span class="nav-wallet-icon"></span>
<span class="nav-wallet-label">CONNECT</span>
`;
}
// Close dropdown on state change
closeWalletDropdown();
}
function toggleWalletDropdown() {
const dd = document.getElementById('navWalletDropdown');
if (!dd) return;
if (dd.classList.contains('hidden')) {
openWalletDropdown();
} else {
closeWalletDropdown();
}
}
function openWalletDropdown() {
const dd = document.getElementById('navWalletDropdown');
if (!dd) return;
const sw = window.solWallet;
if (!sw) {
dd.innerHTML = '<div class="nw-dd-empty">WALLET MODULE NOT LOADED</div>';
dd.classList.remove('hidden');
return;
}
if (sw.connected && sw.address) {
// ── Connected dropdown: address, copy, solscan, disconnect ──
const addr = sw.address;
const solscan = 'https://solscan.io/account/' + addr;
dd.innerHTML = `
<div class="nw-dd-section">
<div class="nw-dd-label">CONNECTED WALLET</div>
<div class="nw-dd-addr" id="nwCopyAddr" title="Click to copy">
<span class="nw-dd-addr-text">${addr}</span>
<span class="nw-dd-copy-icon"></span>
</div>
<div class="nw-dd-via">VIA ${sw.walletName || 'WALLET'}</div>
</div>
<div class="nw-dd-actions">
<a href="${solscan}" target="_blank" rel="noopener" class="nw-dd-action">
<span>SOLSCAN</span><span></span>
</a>
<button class="nw-dd-action nw-dd-disconnect" id="nwDisconnect">
<span>DISCONNECT</span><span></span>
</button>
</div>
`;
// Copy address
dd.querySelector('#nwCopyAddr').addEventListener('click', () => {
navigator.clipboard.writeText(addr).then(() => {
const el = dd.querySelector('#nwCopyAddr .nw-dd-copy-icon');
if (el) { el.textContent = '✓'; setTimeout(() => el.textContent = '⧉', 1500); }
});
});
// Disconnect
dd.querySelector('#nwDisconnect').addEventListener('click', () => {
sw.disconnect();
});
} else {
// ── Disconnected dropdown: wallet picker ──
// Short delay for wallet detection
setTimeout(() => {
const available = sw.getAvailableWallets();
const detectedNames = new Set(available.map(w => w.name));
let html = '';
if (available.length > 0) {
html += '<div class="nw-dd-label">SELECT WALLET</div>';
html += available.map(w => `
<button class="nw-dd-wallet" data-wallet="${w.name}">
<span class="nw-dd-wallet-icon">${w.icon}</span>
<span class="nw-dd-wallet-name">${w.name}</span>
<span class="nw-dd-wallet-tag">DETECTED</span>
</button>
`).join('');
}
// Show install links for missing wallets
const missing = sw.KNOWN_WALLETS.filter(w => !detectedNames.has(w.name));
if (missing.length > 0 && available.length < 3) {
html += '<div class="nw-dd-divider"></div>';
html += '<div class="nw-dd-label nw-dd-label-dim">INSTALL</div>';
html += missing.slice(0, 3).map(w => `
<a href="${w.url}" target="_blank" rel="noopener" class="nw-dd-wallet nw-dd-wallet-install">
<span class="nw-dd-wallet-icon">${w.icon}</span>
<span class="nw-dd-wallet-name">${w.name}</span>
<span class="nw-dd-wallet-tag">INSTALL </span>
</a>
`).join('');
}
if (available.length === 0) {
html = `
<div class="nw-dd-empty">
NO WALLETS DETECTED<br>
<span class="nw-dd-empty-hint">Install a Solana wallet extension</span>
</div>
`;
// Still show install links
html += sw.KNOWN_WALLETS.slice(0, 4).map(w => `
<a href="${w.url}" target="_blank" rel="noopener" class="nw-dd-wallet nw-dd-wallet-install">
<span class="nw-dd-wallet-icon">${w.icon}</span>
<span class="nw-dd-wallet-name">${w.name}</span>
<span class="nw-dd-wallet-tag">INSTALL </span>
</a>
`).join('');
}
dd.innerHTML = html;
// Bind detected wallet clicks
dd.querySelectorAll('.nw-dd-wallet[data-wallet]').forEach(el => {
el.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const name = el.dataset.wallet;
const wallet = available.find(w => w.name === name);
if (!wallet) return;
// Show connecting state
el.querySelector('.nw-dd-wallet-tag').textContent = 'CONNECTING...';
try {
await sw.connect(wallet);
} catch (err) {
el.querySelector('.nw-dd-wallet-tag').textContent = 'REJECTED';
el.querySelector('.nw-dd-wallet-tag').classList.add('nw-dd-error');
setTimeout(() => {
if (el.querySelector('.nw-dd-wallet-tag')) {
el.querySelector('.nw-dd-wallet-tag').textContent = 'DETECTED';
el.querySelector('.nw-dd-wallet-tag').classList.remove('nw-dd-error');
}
}, 2000);
}
});
});
}, 150);
}
dd.classList.remove('hidden');
}
function closeWalletDropdown() {
const dd = document.getElementById('navWalletDropdown');
if (dd) dd.classList.add('hidden');
}
// Run on DOM ready // Run on DOM ready
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {

View file

@ -234,167 +234,39 @@ async function doReverse() {
} }
} }
// ─── WALLET ───────────────────────────────────────────────── // ─── WALLET (uses global window.solWallet from wallet-connect.js) ────────────
let connectedProvider = null;
// ── Wallet Standard Discovery ── // Sync local walletAddress from global wallet state
// MetaMask Solana and modern wallets register via the Wallet Standard protocol function syncWalletState() {
const walletStandardWallets = []; const sw = window.solWallet;
let walletStandardReady = false; if (sw && sw.connected && sw.address) {
walletAddress = sw.address;
function initWalletStandard() { } else {
// Listen for wallets that register AFTER us walletAddress = null;
window.addEventListener('wallet-standard:register-wallet', (event) => {
try {
const callback = event.detail;
if (typeof callback === 'function') {
callback({
register: (...wallets) => {
for (const w of wallets) {
if (!walletStandardWallets.some(existing => existing.name === w.name)) {
walletStandardWallets.push(w);
} }
} }
}
});
}
} catch(e) { console.warn('WS register error:', e); }
});
// Announce that the app is ready — catches wallets that loaded BEFORE us
try {
const appReadyEvent = new CustomEvent('wallet-standard:app-ready', {
detail: {
register: (...wallets) => {
for (const w of wallets) {
if (!walletStandardWallets.some(existing => existing.name === w.name)) {
walletStandardWallets.push(w);
}
}
}
}
});
window.dispatchEvent(appReadyEvent);
} catch(e) { console.warn('WS app-ready error:', e); }
walletStandardReady = true;
}
// Initialise Wallet Standard immediately
initWalletStandard();
function getAvailableWallets() {
const found = [];
const seen = new Set();
function add(name, icon, provider, url, isWalletStandard) {
if (!provider || seen.has(name)) return;
seen.add(name);
found.push({ name, icon, provider, url, isWalletStandard: !!isWalletStandard });
}
// ── 1. Wallet Standard wallets (MetaMask Solana, etc.) ──
for (const w of walletStandardWallets) {
try {
const hasConnect = w.features?.['standard:connect'];
if (!hasConnect) continue;
const name = (w.name || 'WALLET').toUpperCase();
// Use wallet's icon (base64 data URI) or fallback emoji
let icon = '◈';
if (name.includes('METAMASK')) icon = '🦊';
else if (name.includes('PHANTOM')) icon = '👻';
else if (name.includes('SOLFLARE')) icon = '🔆';
else if (name.includes('BACKPACK')) icon = '🎒';
else if (name.includes('COINBASE')) icon = '🔵';
else if (name.includes('TRUST')) icon = '🛡️';
else if (name.includes('JUPITER')) icon = '🪐';
const url = w.url || '#';
add(name, icon, w, url, true);
} catch(e) {}
}
// ── 2. Legacy injection points ──
// Phantom
try {
const p = window.phantom?.solana;
if (p && p.isPhantom) add('PHANTOM', '👻', p, 'https://phantom.app');
} catch(e) {}
// Solflare
try {
const s = window.solflare;
if (s && s.isSolflare) add('SOLFLARE', '🔆', s, 'https://solflare.com');
} catch(e) {}
// Backpack
try {
const b = window.backpack;
if (b && b.isBackpack) add('BACKPACK', '🎒', b, 'https://backpack.app');
} catch(e) {}
// Coinbase
try {
if (window.coinbaseSolana) add('COINBASE', '🔵', window.coinbaseSolana, 'https://coinbase.com/wallet');
} catch(e) {}
// Trust
try {
const t = window.trustwallet?.solana;
if (t) add('TRUST', '🛡️', t, 'https://trustwallet.com');
} catch(e) {}
// ── 3. Generic window.solana ──
try {
const ws = window.solana;
if (ws && !seen.has('SOLANA WALLET')) {
if (ws.isJupiter && !seen.has('JUPITER')) add('JUPITER', '🪐', ws, 'https://jup.ag');
else if (ws.isMetaMask && !seen.has('METAMASK')) add('METAMASK', '🦊', ws, 'https://metamask.io');
else if (ws.isPhantom && !seen.has('PHANTOM')) { /* skip */ }
else if (ws.isSolflare && !seen.has('SOLFLARE')){ /* skip */ }
else if (ws.isBackpack && !seen.has('BACKPACK')){ /* skip */ }
else if (!seen.has('PHANTOM') && !seen.has('METAMASK')) add('SOLANA WALLET', '◈', ws, '#');
}
} catch(e) {}
// ── 4. Jupiter specific ──
try {
const j = window.jupiter?.solana;
if (j) add('JUPITER', '🪐', j, 'https://jup.ag');
} catch(e) {}
// ── 5. Multi-provider array ──
try {
const providers = window.solana?.providers;
if (Array.isArray(providers)) {
for (const p of providers) {
if (p.isPhantom) add('PHANTOM', '👻', p, 'https://phantom.app');
else if (p.isJupiter) add('JUPITER', '🪐', p, 'https://jup.ag');
else if (p.isSolflare) add('SOLFLARE', '🔆', p, 'https://solflare.com');
else if (p.isBackpack) add('BACKPACK', '🎒', p, 'https://backpack.app');
else if (p.isMetaMask) add('METAMASK', '🦊', p, 'https://metamask.io');
}
}
} catch(e) {}
return found;
}
const KNOWN_WALLETS = [
{ name: 'PHANTOM', icon: '👻', url: 'https://phantom.app' },
{ name: 'JUPITER', icon: '🪐', url: 'https://jup.ag' },
{ name: 'SOLFLARE', icon: '🔆', url: 'https://solflare.com' },
{ name: 'BACKPACK', icon: '🎒', url: 'https://backpack.app' },
{ name: 'COINBASE', icon: '🔵', url: 'https://coinbase.com/wallet' },
{ name: 'TRUST', icon: '🛡️', url: 'https://trustwallet.com' },
{ name: 'METAMASK', icon: '🦊', url: 'https://metamask.io' },
];
function initWallet() { function initWallet() {
const btn = $('#wallet-btn'); const btn = $('#wallet-btn');
if (!btn) return; if (!btn) return;
btn.addEventListener('click', toggleWallet); btn.addEventListener('click', toggleWallet);
createWalletModal(); createWalletModal();
// Listen for global wallet events
window.addEventListener('wallet-connected', (e) => {
walletAddress = e.detail.address;
updateWalletUI();
checkPendingRegistration();
});
window.addEventListener('wallet-disconnected', () => {
walletAddress = null;
updateWalletUI();
});
// Sync initial state (wallet may have auto-reconnected already)
syncWalletState();
updateWalletUI();
} }
function createWalletModal() { function createWalletModal() {
@ -420,12 +292,17 @@ function createWalletModal() {
} }
function openModal() { function openModal() {
const sw = window.solWallet;
if (!sw) { console.warn('solWallet not loaded'); return; }
const modal = $('#wallet-modal'); const modal = $('#wallet-modal');
const list = $('#wallet-list'); const list = $('#wallet-list');
const KNOWN_WALLETS = sw.KNOWN_WALLETS || [];
// Small delay to let async provider injection complete // Small delay to let async provider injection complete
setTimeout(() => { setTimeout(() => {
const available = getAvailableWallets(); const available = sw.getAvailableWallets();
const detectedNames = new Set(available.map(w => w.name)); const detectedNames = new Set(available.map(w => w.name));
let html = ''; let html = '';
@ -464,12 +341,21 @@ function openModal() {
list.innerHTML = html; list.innerHTML = html;
list.querySelectorAll('.sol-wallet-option.detected').forEach(btn => { list.querySelectorAll('.sol-wallet-option.detected').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', async (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const name = btn.dataset.wallet; const name = btn.dataset.wallet;
const wallet = available.find(w => w.name === name); const wallet = available.find(w => w.name === name);
if (wallet) connectWallet(wallet); if (wallet) {
try {
closeModal();
await sw.connect(wallet);
} catch (err) {
console.error('Wallet connection failed:', err);
const status = $('#wallet-status');
if (status) status.innerHTML = `<span style="color:rgba(255,50,50,0.8)">CONNECTION REJECTED</span>`;
}
}
}); });
}); });
@ -490,44 +376,10 @@ async function toggleWallet() {
openModal(); openModal();
} }
async function connectWallet(wallet) {
try {
closeModal();
if (wallet.isWalletStandard) {
// Wallet Standard connect (MetaMask Solana, etc.)
const connectFeature = wallet.provider.features['standard:connect'];
const result = await connectFeature.connect();
const accounts = result.accounts || [];
if (accounts.length > 0) {
walletAddress = accounts[0].address;
connectedProvider = wallet;
updateWalletUI();
checkPendingRegistration();
} else {
throw new Error('No accounts returned');
}
} else {
// Legacy provider connect (Phantom, Solflare, etc.)
const resp = await wallet.provider.connect();
walletAddress = resp.publicKey.toString();
connectedProvider = wallet;
updateWalletUI();
checkPendingRegistration();
}
} catch(err) {
console.error('Wallet connection failed:', err);
const status = $('#wallet-status');
if (status) status.innerHTML = `<span style="color:rgba(255,50,50,0.8)">CONNECTION REJECTED</span>`;
}
}
function disconnectWallet() { function disconnectWallet() {
if (connectedProvider && connectedProvider.provider) { const sw = window.solWallet;
try { connectedProvider.provider.disconnect(); } catch(e) {} if (sw) sw.disconnect();
}
walletAddress = null; walletAddress = null;
connectedProvider = null;
updateWalletUI(); updateWalletUI();
} }
@ -535,9 +387,11 @@ function updateWalletUI() {
const status = $('#wallet-status'); const status = $('#wallet-status');
const btn = $('#wallet-btn'); const btn = $('#wallet-btn');
const sw = window.solWallet;
if (walletAddress) { if (walletAddress) {
const wName = connectedProvider ? connectedProvider.name : 'WALLET'; const wName = (sw && sw.walletName) ? sw.walletName : 'WALLET';
const wIcon = connectedProvider ? connectedProvider.icon : '◈'; const wIcon = '◆';
status.className = 'sol-wallet-status connected'; status.className = 'sol-wallet-status connected';
status.innerHTML = `● CONNECTED VIA ${wIcon} ${wName} <span class="sol-wallet-address">${truncAddr(walletAddress)}</span>`; status.innerHTML = `● CONNECTED VIA ${wIcon} ${wName} <span class="sol-wallet-address">${truncAddr(walletAddress)}</span>`;
btn.className = 'sol-wallet-btn disconnect'; btn.className = 'sol-wallet-btn disconnect';
@ -784,7 +638,7 @@ function updateRegistrationStatus(state, message, extra) {
} }
async function executeRegistration(domain) { async function executeRegistration(domain) {
if (!walletAddress || !connectedProvider) { if (!walletAddress || !window.solWallet?.provider) {
updateRegistrationStatus('error', 'WALLET NOT CONNECTED', updateRegistrationStatus('error', 'WALLET NOT CONNECTED',
'<p class="sol-reg-error-detail">Please connect your wallet and try again.</p>'); '<p class="sol-reg-error-detail">Please connect your wallet and try again.</p>');
return; return;
@ -854,7 +708,7 @@ async function executeRegistration(domain) {
// 6. Sign and send via connected wallet // 6. Sign and send via connected wallet
let signature; let signature;
if (connectedProvider.isWalletStandard) { if (window.solWallet && window.solWallet.isWalletStandard) {
signature = await signAndSendWalletStandard(transaction, isVersioned, connection); signature = await signAndSendWalletStandard(transaction, isVersioned, connection);
} else { } else {
signature = await signAndSendLegacy(transaction, isVersioned, connection); signature = await signAndSendLegacy(transaction, isVersioned, connection);
@ -923,7 +777,7 @@ async function executeRegistration(domain) {
} }
async function signAndSendLegacy(transaction, isVersioned, connection) { async function signAndSendLegacy(transaction, isVersioned, connection) {
const provider = connectedProvider.provider; const provider = window.solWallet.provider;
// Try signAndSendTransaction first (Phantom, Solflare support this) // Try signAndSendTransaction first (Phantom, Solflare support this)
if (typeof provider.signAndSendTransaction === 'function') { if (typeof provider.signAndSendTransaction === 'function') {
@ -953,7 +807,7 @@ async function signAndSendLegacy(transaction, isVersioned, connection) {
} }
async function signAndSendWalletStandard(transaction, isVersioned, connection) { async function signAndSendWalletStandard(transaction, isVersioned, connection) {
const wallet = connectedProvider.provider; const wallet = window.solWallet.provider;
const account = wallet.accounts?.[0]; const account = wallet.accounts?.[0];
if (!account) throw new Error('No wallet account available.'); if (!account) throw new Error('No wallet account available.');

268
js/wallet-connect.js Normal file
View file

@ -0,0 +1,268 @@
/* Sitewide Solana Wallet Connect
Global wallet connection module for jaeswift.xyz
Provides window.solWallet API + custom events
*/
(function () {
'use strict';
const LS_WALLET_NAME = 'sol_wallet_name';
const LS_WALLET_ADDR = 'sol_wallet_address';
// ── State ──
const state = {
connected: false,
address: null,
provider: null,
_walletName: null,
_isWalletStandard: false,
};
// ── Wallet Standard Discovery ──
const walletStandardWallets = [];
function initWalletStandard() {
// Listen for wallets registering AFTER us
window.addEventListener('wallet-standard:register-wallet', (event) => {
try {
const callback = event.detail;
if (typeof callback === 'function') {
callback({
register: (...wallets) => {
for (const w of wallets) {
if (!walletStandardWallets.some(e => e.name === w.name)) {
walletStandardWallets.push(w);
}
}
}
});
}
} catch (e) { /* silent */ }
});
// Announce app-ready — catches wallets loaded BEFORE us
try {
window.dispatchEvent(new CustomEvent('wallet-standard:app-ready', {
detail: {
register: (...wallets) => {
for (const w of wallets) {
if (!walletStandardWallets.some(e => e.name === w.name)) {
walletStandardWallets.push(w);
}
}
}
}
}));
} catch (e) { /* silent */ }
}
initWalletStandard();
// ── Known wallets for install links ──
const KNOWN_WALLETS = [
{ name: 'PHANTOM', icon: '👻', url: 'https://phantom.app' },
{ name: 'JUPITER', icon: '🪐', url: 'https://jup.ag' },
{ name: 'SOLFLARE', icon: '🔆', url: 'https://solflare.com' },
{ name: 'BACKPACK', icon: '🎒', url: 'https://backpack.app' },
{ name: 'COINBASE', icon: '🔵', url: 'https://coinbase.com/wallet' },
{ name: 'TRUST', icon: '🛡️', url: 'https://trustwallet.com' },
{ name: 'METAMASK', icon: '🦊', url: 'https://metamask.io' },
];
// ── Wallet detection (reused from soldomains.js) ──
function getAvailableWallets() {
const found = [];
const seen = new Set();
function add(name, icon, provider, url, isWS) {
if (!provider || seen.has(name)) return;
seen.add(name);
found.push({ name, icon, provider, url, isWalletStandard: !!isWS });
}
// 1. Wallet Standard wallets
for (const w of walletStandardWallets) {
try {
if (!w.features?.['standard:connect']) continue;
const name = (w.name || 'WALLET').toUpperCase();
let icon = '◈';
if (name.includes('METAMASK')) icon = '🦊';
else if (name.includes('PHANTOM')) icon = '👻';
else if (name.includes('SOLFLARE')) icon = '🔆';
else if (name.includes('BACKPACK')) icon = '🎒';
else if (name.includes('COINBASE')) icon = '🔵';
else if (name.includes('TRUST')) icon = '🛡️';
else if (name.includes('JUPITER')) icon = '🪐';
add(name, icon, w, w.url || '#', true);
} catch (e) { /* silent */ }
}
// 2. Legacy injection points
try { const p = window.phantom?.solana; if (p && p.isPhantom) add('PHANTOM', '👻', p, 'https://phantom.app'); } catch (e) {}
try { const s = window.solflare; if (s && s.isSolflare) add('SOLFLARE', '🔆', s, 'https://solflare.com'); } catch (e) {}
try { const b = window.backpack; if (b && b.isBackpack) add('BACKPACK', '🎒', b, 'https://backpack.app'); } catch (e) {}
try { if (window.coinbaseSolana) add('COINBASE', '🔵', window.coinbaseSolana, 'https://coinbase.com/wallet'); } catch (e) {}
try { const t = window.trustwallet?.solana; if (t) add('TRUST', '🛡️', t, 'https://trustwallet.com'); } catch (e) {}
// 3. Generic window.solana
try {
const ws = window.solana;
if (ws) {
if (ws.isJupiter && !seen.has('JUPITER')) add('JUPITER', '🪐', ws, 'https://jup.ag');
else if (ws.isMetaMask && !seen.has('METAMASK')) add('METAMASK', '🦊', ws, 'https://metamask.io');
else if (!ws.isPhantom && !ws.isSolflare && !ws.isBackpack && !seen.has('PHANTOM') && !seen.has('METAMASK'))
add('SOLANA WALLET', '◈', ws, '#');
}
} catch (e) {}
// 4. Jupiter specific
try { const j = window.jupiter?.solana; if (j) add('JUPITER', '🪐', j, 'https://jup.ag'); } catch (e) {}
// 5. Multi-provider array
try {
const providers = window.solana?.providers;
if (Array.isArray(providers)) {
for (const p of providers) {
if (p.isPhantom) add('PHANTOM', '👻', p, 'https://phantom.app');
else if (p.isJupiter) add('JUPITER', '🪐', p, 'https://jup.ag');
else if (p.isSolflare) add('SOLFLARE', '🔆', p, 'https://solflare.com');
else if (p.isBackpack) add('BACKPACK', '🎒', p, 'https://backpack.app');
else if (p.isMetaMask) add('METAMASK', '🦊', p, 'https://metamask.io');
}
}
} catch (e) {}
return found;
}
// ── Connect ──
async function connect(walletObj) {
// If called without arg, open picker (handled by nav UI)
// If called with a wallet name string, find it
if (typeof walletObj === 'string') {
const available = getAvailableWallets();
walletObj = available.find(w => w.name === walletObj);
if (!walletObj) throw new Error('Wallet not found: ' + walletObj);
}
if (!walletObj) throw new Error('No wallet specified');
try {
let address;
if (walletObj.isWalletStandard) {
const connectFeature = walletObj.provider.features['standard:connect'];
const result = await connectFeature.connect();
const accounts = result.accounts || [];
if (accounts.length === 0) throw new Error('No accounts returned');
address = accounts[0].address;
} else {
const resp = await walletObj.provider.connect();
address = resp.publicKey.toString();
}
state.connected = true;
state.address = address;
state.provider = walletObj.provider;
state._walletName = walletObj.name;
state._isWalletStandard = walletObj.isWalletStandard;
// Persist
localStorage.setItem(LS_WALLET_NAME, walletObj.name);
localStorage.setItem(LS_WALLET_ADDR, address);
// Dispatch event
window.dispatchEvent(new CustomEvent('wallet-connected', {
detail: { address, walletName: walletObj.name }
}));
return { address, walletName: walletObj.name };
} catch (err) {
console.warn('[solWallet] Connect failed:', err.message);
throw err;
}
}
// ── Disconnect ──
function disconnect() {
if (state.provider) {
try {
if (state._isWalletStandard) {
const disconnectFeature = state.provider.features?.['standard:disconnect'];
if (disconnectFeature) disconnectFeature.disconnect();
} else {
state.provider.disconnect();
}
} catch (e) { /* silent */ }
}
const prevAddr = state.address;
state.connected = false;
state.address = null;
state.provider = null;
state._walletName = null;
state._isWalletStandard = false;
localStorage.removeItem(LS_WALLET_NAME);
localStorage.removeItem(LS_WALLET_ADDR);
window.dispatchEvent(new CustomEvent('wallet-disconnected', {
detail: { address: prevAddr }
}));
}
// ── Silent auto-reconnect ──
async function autoReconnect() {
const savedName = localStorage.getItem(LS_WALLET_NAME);
if (!savedName) return;
// Small delay to let wallet extensions inject
await new Promise(r => setTimeout(r, 600));
const available = getAvailableWallets();
const wallet = available.find(w => w.name === savedName);
if (!wallet) {
// Wallet no longer available — clear saved state quietly
localStorage.removeItem(LS_WALLET_NAME);
localStorage.removeItem(LS_WALLET_ADDR);
return;
}
try {
await connect(wallet);
} catch (e) {
// Auto-reconnect failed — clear saved state quietly
localStorage.removeItem(LS_WALLET_NAME);
localStorage.removeItem(LS_WALLET_ADDR);
state.connected = false;
state.address = null;
state.provider = null;
state._walletName = null;
}
}
// ── Truncate address helper ──
function truncAddr(a) {
if (!a || a.length < 12) return a || '';
return a.slice(0, 4) + '...' + a.slice(-4);
}
// ── Public API ──
window.solWallet = {
get connected() { return state.connected; },
get address() { return state.address; },
get provider() { return state.provider; },
get walletName() { return state._walletName; },
get isWalletStandard() { return state._isWalletStandard; },
connect,
disconnect,
getAvailableWallets,
truncAddr,
KNOWN_WALLETS,
_autoReconnect: autoReconnect,
};
// Kick off auto-reconnect when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', autoReconnect);
} else {
autoReconnect();
}
})();

View file

@ -110,6 +110,7 @@
</footer> </footer>
<script src="/js/post.js"></script> <script src="/js/post.js"></script>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
</body> </body>
</html> </html>

View file

@ -61,6 +61,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
<script src="/js/awesomelist.js"></script> <script src="/js/awesomelist.js"></script>

View file

@ -113,6 +113,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
<script src="https://unpkg.com/@solana/web3.js@1.98.0/lib/index.iife.min.js"></script> <script src="https://unpkg.com/@solana/web3.js@1.98.0/lib/index.iife.min.js"></script>

View file

@ -75,6 +75,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
<script src="/js/blog.js"></script> <script src="/js/blog.js"></script>

View file

@ -69,6 +69,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>

View file

@ -84,6 +84,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
<script src="/js/radar.js?v=20260404"></script> <script src="/js/radar.js?v=20260404"></script>

View file

@ -63,6 +63,7 @@
</div> </div>
</footer> </footer>
<script src="/js/wallet-connect.js"></script>
<script src="/js/nav.js"></script> <script src="/js/nav.js"></script>
<script src="/js/clock.js"></script> <script src="/js/clock.js"></script>
</body> </body>