// Configuration const RARIBLE_API_KEY = 'd01c6b48-8f7a-4f58-8a1e-5c5a5d9c5b9a'; // New test API key const RARIBLE_API_BASE = 'https://api.rarible.org/v0.1'; const CONTRACT_ADDRESS = 'ETHEREUM:0x60f80121c31a0d46b5279700f9df786054aa5ee5'; // New test contract // State let state = { walletAddress: null, nfts: [], filteredNFTs: [], isLoading: false, currentFilter: 'all' }; // DOM Elements const elements = { connectWalletBtn: document.getElementById('connectWallet'), disconnectWalletBtn: document.getElementById('disconnectWallet'), walletAddressEl: document.getElementById('walletAddress'), walletInfoEl: document.getElementById('walletInfo'), nftContainer: document.getElementById('nftContainer'), searchInput: document.getElementById('searchInput'), filterButtons: document.querySelectorAll('.filter-btn'), refreshBtn: document.getElementById('refreshNFTs'), statusMessage: document.getElementById('statusMessage') }; // Initialize document.addEventListener('DOMContentLoaded', async () => { await checkWalletConnection(); setupEventListeners(); }); // Check if wallet is already connected async function checkWalletConnection() { if (typeof window.ethereum === 'undefined') { window.demoMode = true; showStatus('No wallet detected - using demo mode', 'info'); return; } try { const accounts = await window.ethereum.request({ method: 'eth_accounts' }); if (accounts.length > 0) { state.walletAddress = accounts[0]; updateWalletDisplay(); showStatus('Wallet connected! Click Refresh to load NFTs.', 'success'); } else { window.demoMode = true; showStatus('Connect wallet or use demo mode', 'info'); } } catch (error) { console.error('Error checking wallet:', error); window.demoMode = true; showStatus('Using demo mode', 'info'); } } // Connect Wallet elements.connectWalletBtn.addEventListener('click', async () => { if (typeof window.ethereum === 'undefined') { window.demoMode = true; showStatus('No wallet detected - using demo mode', 'info'); fetchNFTs(); return; } try { elements.connectWalletBtn.disabled = true; elements.connectWalletBtn.textContent = 'Connecting...'; showStatus('Connecting to wallet...', 'info'); const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); if (accounts.length > 0) { state.walletAddress = accounts[0]; window.demoMode = false; updateWalletDisplay(); showStatus('Wallet connected! Click Refresh to load NFTs.', 'success'); window.ethereum.on('accountsChanged', handleAccountsChanged); } else { window.demoMode = true; showStatus('Using demo mode', 'info'); } } catch (error) { console.error('Error connecting wallet:', error); window.demoMode = true; showStatus('Using demo mode', 'info'); fetchNFTs(); } finally { elements.connectWalletBtn.disabled = false; elements.connectWalletBtn.textContent = 'Connect Wallet'; } }); // Disconnect Wallet elements.disconnectWalletBtn.addEventListener('click', () => { disconnectWallet(); }); // Handle account changes function handleAccountsChanged(accounts) { if (accounts.length === 0) { disconnectWallet(); showStatus('Wallet disconnected', 'info'); } else { state.walletAddress = accounts[0]; updateWalletDisplay(); showStatus('Wallet account changed', 'info'); // Optionally reload NFTs fetchNFTs(); } } // Disconnect wallet function function disconnectWallet() { state.walletAddress = null; state.nfts = []; state.filteredNFTs = []; window.demoMode = true; if (window.ethereum?.removeListener) { window.ethereum.removeListener('accountsChanged', handleAccountsChanged); } elements.walletInfoEl.style.display = 'none'; elements.connectWalletBtn.style.display = 'block'; elements.connectWalletBtn.disabled = false; fetchNFTs(); // Will fall back to demo mode showStatus('Wallet disconnected - using demo mode', 'info'); } // Update wallet display function updateWalletDisplay() { if (state.walletAddress) { const shortAddress = `${state.walletAddress.substring(0, 6)}...${state.walletAddress.substring(38)}`; elements.walletAddressEl.textContent = shortAddress; elements.walletInfoEl.style.display = 'flex'; elements.connectWalletBtn.style.display = 'none'; } } // Show status message function showStatus(message, type = 'info') { elements.statusMessage.textContent = message; elements.statusMessage.className = `status-message ${type}`; elements.statusMessage.style.display = 'block'; // Auto-hide after 5 seconds setTimeout(() => { elements.statusMessage.style.display = 'none'; }, 5000); } // Fetch NFTs from Rarible API async function fetchNFTs() { if (state.isLoading) { showStatus('Already loading NFTs...', 'info'); return; } if (!state.walletAddress && !window.demoMode) { showStatus('Please connect your wallet first', 'error'); return; } try { state.isLoading = true; elements.refreshBtn.disabled = true; elements.refreshBtn.textContent = 'Loading...'; showStatus('Loading NFTs...', 'info'); renderLoadingState(); if (window.demoMode) { // Use demo data const demoNFTs = [ { id: 'demo1', meta: { name: 'Cosmic Explorer #1', description: 'A demo NFT for testing purposes', image: 'http://static.photos/abstract/640x360/1' }, sellOrders: [{ id: 'order1', take: { value: '1000000000000000000', // 1 ETH type: { '@type': 'ETH', decimals: 18 } } }] }, { id: 'demo2', meta: { name: 'Stellar Artwork', description: 'Another demo NFT for testing', image: 'http://static.photos/abstract/640x360/2' }, sellOrders: [] }, { id: 'demo3', meta: { name: 'Quantum Pixel', description: 'Digital art piece', image: 'http://static.photos/abstract/640x360/3' }, sellOrders: [{ id: 'order3', take: { value: '250000000000000000', // 0.25 ETH type: { '@type': 'ETH', decimals: 18 } } }] } ]; state.nfts = demoNFTs; state.filteredNFTs = [...demoNFTs]; applyFilter(state.currentFilter); showStatus('Demo mode: Loaded 3 test NFTs', 'success'); return; } // Real API fetch const encodedWallet = encodeURIComponent(`ETHEREUM:${state.walletAddress}`); const apiUrl = `${RARIBLE_API_BASE}/items/byOwner?owner=${encodedWallet}&size=50`; const response = await fetch(apiUrl, { headers: { 'X-API-KEY': RARIBLE_API_KEY, 'Accept': 'application/json' } }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } const data = await response.json(); state.nfts = data.items || []; state.filteredNFTs = [...state.nfts]; if (state.nfts.length === 0) { renderEmptyState('No NFTs found in your wallet'); showStatus('No NFTs found in your wallet', 'info'); return; } applyFilter(state.currentFilter); showStatus(`Loaded ${state.nfts.length} NFTs`, 'success'); } catch (error) { console.error('Error:', error); window.demoMode = true; fetchNFTs(); // Fallback to demo mode } finally { state.isLoading = false; elements.refreshBtn.disabled = false; elements.refreshBtn.textContent = '🔄 Refresh NFTs'; } } // Enrich NFT with order data async function enrichNFTWithOrderData(nft) { try { const itemId = encodeURIComponent(nft.id); // Get sell orders for this NFT const ordersResponse = await fetch( `${RARIBLE_API_BASE}/orders/items/${itemId}/sell`, { headers: { 'X-API-KEY': RARIBLE_API_KEY, 'Accept': 'application/json' } } ); if (ordersResponse.ok) { const ordersData = await ordersResponse.json(); nft.sellOrders = (ordersData.orders || []).filter(order => order.status === 'ACTIVE' && order.makeStock > 0 // Ensure there's available stock ); // Get best price (lowest) if (nft.sellOrders.length > 0) { nft.bestOrder = nft.sellOrders.reduce((best, current) => { const bestPrice = best ? parseFloat(best.take.value) : Infinity; const currentPrice = current ? parseFloat(current.take.value) : Infinity; return currentPrice < bestPrice ? current : best; }); } } else { nft.sellOrders = []; nft.bestOrder = null; } } catch (error) { console.error('Error enriching NFT:', nft.id, error); nft.sellOrders = []; nft.bestOrder = null; } } // Render NFTs without flickering function renderNFTs() { if (!elements.nftContainer) return; if (state.filteredNFTs.length === 0) { renderEmptyState('No NFTs match your filter'); return; } // Clear and render in one operation to prevent flickering const fragment = document.createDocumentFragment(); state.filteredNFTs.forEach((nft, index) => { const card = createNFTCard(nft, index); fragment.appendChild(card); }); elements.nftContainer.innerHTML = ''; elements.nftContainer.appendChild(fragment); } // Create NFT Card Element function createNFTCard(nft, index) { const card = document.createElement('div'); card.className = 'nft-card'; card.style.animationDelay = `${index * 0.05}s`; // Check if NFT is for sale const isForSale = nft.sellOrders && nft.sellOrders.length > 0 && nft.bestOrder; let price = 'Not Listed'; let priceValue = 0; if (isForSale && nft.bestOrder.take) { const decimals = nft.bestOrder.take.type?.decimals || 18; priceValue = parseFloat(nft.bestOrder.take.value) / Math.pow(10, decimals); const currency = nft.bestOrder.take.type?.['@type'] === 'ETH' ? 'ETH' : nft.bestOrder.take.assetType?.assetClass || 'ETH'; price = `${priceValue.toFixed(4)} ${currency}`; } // Get image URL let imageUrl = getImageUrl(nft); const nftName = nft.meta?.name || `NFT #${nft.tokenId || 'Unknown'}`; const description = nft.meta?.description || 'No description available'; card.innerHTML = `
${escapeHtml(nftName)}

${escapeHtml(nftName)}

${escapeHtml(description)}

${price}
`; // Add click event to buy button const buyButton = card.querySelector('.buy-button'); if (buyButton && isForSale) { buyButton.addEventListener('click', () => handleBuy(nft.id, nft.bestOrder, priceValue)); } return card; } // Get image URL from NFT metadata function getImageUrl(nft) { let imageUrl = 'https://via.placeholder.com/400x400/14141f/a0a0b8?text=Loading...'; if (nft.meta?.image) { if (typeof nft.meta.image === 'string') { imageUrl = nft.meta.image; } else if (nft.meta.image?.url) { imageUrl = nft.meta.image.url.ORIGINAL || nft.meta.image.url.BIG || nft.meta.image.url.PREVIEW || nft.meta.image.url; } else if (nft.meta.image?.PREVIEW) { imageUrl = nft.meta.image.PREVIEW; } } // Fallback to content array if (imageUrl.includes('placeholder') && nft.meta?.content && Array.isArray(nft.meta.content)) { const imageContent = nft.meta.content.find(item => item['@type'] === 'IMAGE' || item.type === 'image' ); if (imageContent?.url) { imageUrl = imageContent.url; } } // Convert IPFS URLs if (imageUrl.startsWith('ipfs://')) { imageUrl = imageUrl.replace('ipfs://', 'https://ipfs.io/ipfs/'); } return imageUrl; } // Escape HTML to prevent XSS function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Handle Buy Function async function handleBuy(nftId, order, price) { if (!window.ethereum || !state.walletAddress) { showStatus('Please connect wallet first', 'error'); return; } try { const buyButton = document.querySelector(`[data-nft-id="${nftId}"]`); if (!buyButton) return; const originalText = buyButton.textContent; buyButton.disabled = true; buyButton.textContent = 'Processing...'; showStatus('Starting purchase process...', 'info'); // In production, you would integrate with Rarible SDK or Web3 // For demo purposes: const confirmBuy = confirm( `DEMO MODE\n\n` + `This would purchase the NFT for ${price} ETH.\n\n` + `In production, this would:\n` + `1. Verify the order is still active\n` + `2. Request approval for payment token\n` + `3. Execute the purchase transaction\n` + `4. Transfer the NFT to your wallet\n\n` + `Continue with demo?` ); if (confirmBuy) { showStatus('Demo purchase initiated! In production, this would trigger a blockchain transaction.', 'success'); setTimeout(() => { buyButton.textContent = '✓ Demo Complete'; buyButton.style.background = 'linear-gradient(135deg, #00ff88 0%, #00cc66 100%)'; }, 1500); } else { buyButton.disabled = false; buyButton.textContent = originalText; } } catch (error) { console.error('Error in purchase process:', error); showStatus('Error in purchase process', 'error'); const buyButton = document.querySelector(`[data-nft-id="${nftId}"]`); if (buyButton) { buyButton.disabled = false; buyButton.textContent = `Buy for ${price} ETH`; } } } // Apply filter function applyFilter(filter) { state.currentFilter = filter; switch(filter) { case 'for-sale': state.filteredNFTs = state.nfts.filter(nft => nft.sellOrders && nft.sellOrders.length > 0 ); break; case 'not-for-sale': state.filteredNFTs = state.nfts.filter(nft => !nft.sellOrders || nft.sellOrders.length === 0 ); break; default: state.filteredNFTs = [...state.nfts]; } renderNFTs(); showStatus(`Showing ${state.filteredNFTs.length} NFTs`, 'info'); } // Search NFTs function searchNFTs(searchTerm) { const term = searchTerm.toLowerCase().trim(); if (term === '') { state.filteredNFTs = [...state.nfts]; } else { state.filteredNFTs = state.nfts.filter(nft => { const name = (nft.meta?.name || '').toLowerCase(); const description = (nft.meta?.description || '').toLowerCase(); const tokenId = (nft.tokenId || '').toString().toLowerCase(); return name.includes(term) || description.includes(term) || tokenId.includes(term); }); } applyFilter(state.currentFilter); } // Render states function renderLoadingState() { elements.nftContainer.innerHTML = `

Loading your NFTs from the blockchain...

`; } function renderEmptyState(message) { elements.nftContainer.innerHTML = `

No NFTs Found

${message}

`; } function renderErrorState(errorMessage) { elements.nftContainer.innerHTML = `

Error Loading NFTs

${errorMessage}

Please check your connection and try again.

`; } // Setup Event Listeners function setupEventListeners() { // Refresh button elements.refreshBtn.addEventListener('click', fetchNFTs); // Search with debounce let searchTimeout; elements.searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { searchNFTs(e.target.value); }, 300); }); // Filter buttons elements.filterButtons.forEach(button => { button.addEventListener('click', () => { elements.filterButtons.forEach(btn => btn.classList.remove('active')); button.classList.add('active'); const filter = button.dataset.filter; applyFilter(filter); }); }); } // Debug utilities window.debug = { getState: () => state, getNFTs: () => state.nfts, getFilteredNFTs: () => state.filteredNFTs, reloadNFTs: fetchNFTs, getSellOrders: (index) => state.nfts[index]?.sellOrders || [] };