Spaces:
Running
Running
| // 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 = ` | |
| <div class="nft-image-container"> | |
| <img src="${imageUrl}" | |
| alt="${escapeHtml(nftName)}" | |
| class="nft-image" | |
| loading="lazy" | |
| onerror="this.src='https://via.placeholder.com/400x400/14141f/a0a0b8?text=NFT'"> | |
| </div> | |
| <div class="nft-info"> | |
| <h3 class="nft-name" title="${escapeHtml(nftName)}">${escapeHtml(nftName)}</h3> | |
| <p class="nft-description">${escapeHtml(description)}</p> | |
| <div class="nft-price ${isForSale ? 'for-sale' : 'not-for-sale'}">${price}</div> | |
| <button class="buy-button" | |
| data-nft-id="${nft.id}" | |
| data-order-id="${isForSale ? nft.bestOrder.id : ''}" | |
| ${!isForSale ? 'disabled' : ''}> | |
| ${!isForSale ? 'Not Listed' : `Buy for ${price}`} | |
| </button> | |
| </div> | |
| `; | |
| // 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 = ` | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| <p>Loading your NFTs from the blockchain...</p> | |
| </div> | |
| `; | |
| } | |
| function renderEmptyState(message) { | |
| elements.nftContainer.innerHTML = ` | |
| <div class="empty-state"> | |
| <h3>No NFTs Found</h3> | |
| <p>${message}</p> | |
| </div> | |
| `; | |
| } | |
| function renderErrorState(errorMessage) { | |
| elements.nftContainer.innerHTML = ` | |
| <div class="error-state"> | |
| <h3>Error Loading NFTs</h3> | |
| <p>${errorMessage}</p> | |
| <p>Please check your connection and try again.</p> | |
| </div> | |
| `; | |
| } | |
| // 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 || [] | |
| }; | |