1/** 2 * NFT Gallery for Captain Ethereum Pre-Series 3 * This script fetches and displays all NFTs from the contract 4 */ 5 6// DOM Elements 7const loadingOverlay = document.getElementById('loadingOverlay'); 8const statusMessage = document.getElementById('statusMessage'); 9const charactersGrid = document.getElementById('charactersGrid'); 10const nftModal = document.getElementById('nftModal'); 11const closeModal = document.getElementById('closeModal'); 12const modalImage = document.getElementById('modalImage'); 13const modalName = document.getElementById('modalName'); 14const modalRole = document.getElementById('modalRole'); 15const modalDescription = document.getElementById('modalDescription'); 16const modalMetadata = document.getElementById('modalMetadata'); 17const modalPowers = document.getElementById('modalPowers'); 18const modalFaction = document.getElementById('modalFaction'); 19 20// Contract configuration 21const CONTRACT_ADDRESS = '0x13BaE33c02b91390b9c3A22E4B1B1F4a065DBD78'; // Replace with your actual contract address 22const CONTRACT_ABI = [ 23 { 24 "inputs": [], 25 "stateMutability": "nonpayable", 26 "type": "constructor" 27 }, 28 { 29 "inputs": [], 30 "name": "InvalidCharacterData", 31 "type": "error" 32 }, 33 { 34 "inputs": [], 35 "name": "InvalidRecipient", 36 "type": "error" 37 }, 38 { 39 "anonymous": false, 40 "inputs": [ 41 { 42 "indexed": true, 43 "internalType": "address", 44 "name": "account", 45 "type": "address" 46 }, 47 { 48 "indexed": true, 49 "internalType": "address", 50 "name": "operator", 51 "type": "address" 52 }, 53 { 54 "indexed": false, 55 "internalType": "bool", 56 "name": "approved", 57 "type": "bool" 58 } 59 ], 60 "name": "ApprovalForAll", 61 "type": "event" 62 }, 63 { 64 "anonymous": false, 65 "inputs": [ 66 { 67 "indexed": true, 68 "internalType": "address", 69 "name": "previousOwner", 70 "type": "address" 71 }, 72 { 73 "indexed": true, 74 "internalType": "address", 75 "name": "newOwner", 76 "type": "address" 77 } 78 ], 79 "name": "OwnershipTransferred", 80 "type": "event" 81 }, 82 { 83 "anonymous": false, 84 "inputs": [ 85 { 86 "indexed": true, 87 "internalType": "address", 88 "name": "operator", 89 "type": "address" 90 }, 91 { 92 "indexed": true, 93 "internalType": "address", 94 "name": "from", 95 "type": "address" 96 }, 97 { 98 "indexed": true, 99 "internalType": "address", 100 "name": "to", 101 "type": "address" 102 }, 103 { 104 "indexed": false, 105 "internalType": "uint256[]", 106 "name": "ids", 107 "type": "uint256[]" 108 }, 109 { 110 "indexed": false, 111 "internalType": "uint256[]", 112 "name": "values", 113 "type": "uint256[]" 114 } 115 ], 116 "name": "TransferBatch", 117 "type": "event" 118 }, 119 { 120 "anonymous": false, 121 "inputs": [ 122 { 123 "indexed": true, 124 "internalType": "address", 125 "name": "operator", 126 "type": "address" 127 }, 128 { 129 "indexed": true, 130 "internalType": "address", 131 "name": "from", 132 "type": "address" 133 }, 134 { 135 "indexed": true, 136 "internalType": "address", 137 "name": "to", 138 "type": "address" 139 }, 140 { 141 "indexed": false, 142 "internalType": "uint256", 143 "name": "id", 144 "type": "uint256" 145 }, 146 { 147 "indexed": false, 148 "internalType": "uint256", 149 "name": "value", 150 "type": "uint256" 151 } 152 ], 153 "name": "TransferSingle", 154 "type": "event" 155 }, 156 { 157 "anonymous": false, 158 "inputs": [ 159 { 160 "indexed": false, 161 "internalType": "string", 162 "name": "value", 163 "type": "string" 164 }, 165 { 166 "indexed": true, 167 "internalType": "uint256", 168 "name": "id", 169 "type": "uint256" 170 } 171 ], 172 "name": "URI", 173 "type": "event" 174 }, 175 { 176 "inputs": [ 177 { 178 "internalType": "address", 179 "name": "account", 180 "type": "address" 181 }, 182 { 183 "internalType": "uint256", 184 "name": "id", 185 "type": "uint256" 186 } 187 ], 188 "name": "balanceOf", 189 "outputs": [ 190 { 191 "internalType": "uint256", 192 "name": "", 193 "type": "uint256" 194 } 195 ], 196 "stateMutability": "view", 197 "type": "function" 198 }, 199 { 200 "inputs": [ 201 { 202 "internalType": "address[]", 203 "name": "accounts", 204 "type": "address[]" 205 }, 206 { 207 "internalType": "uint256[]", 208 "name": "ids", 209 "type": "uint256[]" 210 } 211 ], 212 "name": "balanceOfBatch", 213 "outputs": [ 214 { 215 "internalType": "uint256[]", 216 "name": "", 217 "type": "uint256[]" 218 } 219 ], 220 "stateMutability": "view", 221 "type": "function" 222 }, 223 { 224 "inputs": [ 225 { 226 "internalType": "uint256", 227 "name": "", 228 "type": "uint256" 229 } 230 ], 231 "name": "characters", 232 "outputs": [ 233 { 234 "internalType": "string", 235 "name": "name", 236 "type": "string" 237 }, 238 { 239 "internalType": "string", 240 "name": "description", 241 "type": "string" 242 }, 243 { 244 "internalType": "string", 245 "name": "base64Image", 246 "type": "string" 247 }, 248 { 249 "internalType": "string", 250 "name": "faction", 251 "type": "string" 252 } 253 ], 254 "stateMutability": "view", 255 "type": "function" 256 }, 257 { 258 "inputs": [], 259 "name": "name", 260 "outputs": [ 261 { 262 "internalType": "string", 263 "name": "", 264 "type": "string" 265 } 266 ], 267 "stateMutability": "view", 268 "type": "function" 269 }, 270 { 271 "inputs": [], 272 "name": "nextTokenId", 273 "outputs": [ 274 { 275 "internalType": "uint256", 276 "name": "", 277 "type": "uint256" 278 } 279 ], 280 "stateMutability": "view", 281 "type": "function" 282 }, 283 { 284 "inputs": [ 285 { 286 "internalType": "uint256", 287 "name": "tokenId", 288 "type": "uint256" 289 } 290 ], 291 "name": "uri", 292 "outputs": [ 293 { 294 "internalType": "string", 295 "name": "", 296 "type": "string" 297 } 298 ], 299 "stateMutability": "view", 300 "type": "function" 301 } 302]; 303 304// Web3 initialization 305let web3; 306let contract; 307 308// Initialize Ethereum connection 309async function initWeb3() { 310 // Use Infura or other provider 311 const providerUrl = 'https://mainnet.infura.io/v3/99527ad2c780409d8cc89a32fc315bf7'; // Replace with your Infura key or other provider 312 313 try { 314 web3 = new Web3(new Web3.providers.HttpProvider(providerUrl)); 315 contract = new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS); 316 317 await loadNFTs(); 318 } catch (error) { 319 console.error('Error initializing Web3:', error);
320 statusMessage.textContent = 'Error connecting to blockchain. Please check console for details.'; 321 loadingOverlay.classList.add('hidden'); 322 } 323} 324 325// Load all NFTs from contract 326async function loadNFTs() { 327 try { 328 // Get total NFTs minted 329 const nextTokenId = await contract.methods.nextTokenId().call(); 330 331 if (parseInt(nextTokenId) === 0) { 332 displayEmptyState(); 333 return; 334 } 335 336 // Array to hold all token promises 337 const tokenPromises = []; 338 339 // Fetch data for all tokens 340 for (let i = 0; i < nextTokenId; i++) { 341 tokenPromises.push(fetchTokenData(i)); 342 } 343 344 // Wait for all tokens to be fetched 345 const tokenData = await Promise.all(tokenPromises); 346 347 // Display tokens 348 displayNFTs(tokenData); 349 350 } catch (error) { 351 console.error('Error loading NFTs:', error); 352 statusMessage.textContent = 'Error loading NFTs. Please check console for details.'; 353 } 354 355 // Hide loading overlay 356 loadingOverlay.classList.add('hidden'); 357} 358 359// Fetch metadata for a specific token 360async function fetchTokenData(tokenId) { 361 try { 362 // Get token URI 363 const uri = await contract.methods.uri(tokenId).call(); 364 365 // Parse data URI format (data:application/json;base64,...) 366 const jsonBase64 = uri.split('base64,')[1]; 367 const jsonString = atob(jsonBase64); 368 const metadata = JSON.parse(jsonString); 369 370 // Get additional character data from contract 371 const characterData = await contract.methods.characters(tokenId).call(); 372 373 return { 374 id: tokenId, 375 metadata: metadata, 376 characterData: characterData 377 }; 378 } catch (error) { 379 console.error(`Error fetching data for token ${tokenId}:`, error); 380 return null; 381 } 382} 383 384// Display NFTs in grid 385function displayNFTs(tokenData) { 386 // Filter out any null values from failed fetches 387 const validTokens = tokenData.filter(token => token !== null); 388 389 if (validTokens.length === 0) { 390 displayEmptyState(); 391 return; 392 } 393 394 // Clear status message 395 statusMessage.style.display = 'none'; 396 397 // Clear existing grid 398 charactersGrid.innerHTML = ''; 399 400 // Add each token to the grid 401 validTokens.forEach(token => { 402 const { id, metadata, characterData } = token; 403 404 // Create card element 405 const card = document.createElement('div'); 406 card.className = 'character-card'; 407 card.setAttribute('data-token-id', id); 408 409 // Get role from first core trait if available 410 const role = metadata.attributes && 411 metadata.attributes.find(attr => attr.trait_type === 'Core Trait 1')?.value || 412 'Unknown Role'; 413 414 // Create card HTML 415 card.innerHTML = ` 416 <div class="character-image"> 417 ${getSvgFromBase64(metadata.image)} 418 </div> 419 <div class="character-content"> 420 <h3 class="character-name">${metadata.name}</h3> 421 <p class="character-role">${role}</p> 422 <p class="character-description">${truncateText(metadata.description, 100)}</p> 423 </div> 424 `; 425 426 // Add card to grid 427 charactersGrid.appendChild(card); 428 429 // Add click event to open modal 430 card.addEventListener('click', () => { 431 openNFTModal(token); 432 }); 433 }); 434} 435 436// Display empty state when no NFTs are found 437function displayEmptyState() { 438 charactersGrid.innerHTML = ` 439 <div class="empty-state"> 440 <h3>No NFTs Found</h3> 441 <p>There are currently no Captain Ethereum Pre-Series NFTs in this collection.</p> 442 </div> 443 `; 444 statusMessage.style.display = 'none'; 445} 446 447// Extract SVG from base64 image data 448function getSvgFromBase64(base64Image) { 449 if (!base64Image) return '<div>Image not available</div>'; 450 451 // If it's already SVG data 452 if (base64Image.startsWith('<svg') || base64Image.includes('<svg')) { 453 return base64Image; 454 } 455 456 // If it's a data URI 457 if (base64Image.startsWith('data:')) { 458 // For SVG 459 if (base64Image.includes('data:image/svg+xml;')) { 460 try { 461 const svgBase64 = base64Image.split('base64,')[1]; 462 return atob(svgBase64); 463 } catch (e) { 464 console.error('Error decoding SVG:', e); 465 return '<div>Error loading image</div>'; 466 } 467 } 468 469 // For other image formats, use an img tag 470 return `<img src="${base64Image}" alt="NFT" />`; 471 } 472 473 // If it's a URL 474 return `<img src="${base64Image}" alt="NFT" />`; 475} 476 477// Open modal with NFT details 478function openNFTModal(token) { 479 const { id, metadata, characterData } = token; 480 481 // Set modal content 482 modalImage.innerHTML = getSvgFromBase64(metadata.image); 483 modalName.textContent = metadata.name; 484 485 // Get role from first core trait if available 486 const role = metadata.attributes && 487 metadata.attributes.find(attr => attr.trait_type === 'Core Trait 1')?.value || 488 'Unknown Role'; 489 modalRole.textContent = role; 490 491 // Set description
492 modalDescription.textContent = metadata.description; 493 494 // Set faction 495 modalFaction.textContent = characterData.faction || 'Unknown'; 496 497 // Clear previous metadata 498 modalMetadata.innerHTML = ''; 499 500 // Add core traits to metadata 501 const coreTraits = metadata.attributes.filter(attr => 502 attr.trait_type.startsWith('Core Trait') && 503 attr.value && 504 attr.value !== 'NA' && 505 attr.value !== '-1' && 506 attr.value !== 'Unknown' 507 ); 508 509 if (coreTraits.length > 0) { 510 coreTraits.forEach(trait => { 511 const traitElement = document.createElement('div'); 512 traitElement.className = 'metadata-item'; 513 traitElement.innerHTML = ` 514 <span class="metadata-label">${trait.trait_type}</span> 515 <span class="metadata-value">${trait.value}</span> 516 `; 517 modalMetadata.appendChild(traitElement); 518 }); 519 } 520 521 // Add token ID to metadata 522 const tokenIdElement = document.createElement('div'); 523 tokenIdElement.className = 'metadata-item'; 524 tokenIdElement.innerHTML = ` 525 <span class="metadata-label">Token ID</span> 526 <span class="metadata-value">${id}</span> 527 `; 528 modalMetadata.appendChild(tokenIdElement); 529 530 // Clear previous powers 531 modalPowers.innerHTML = ''; 532 533 // Add powers 534 const powers = metadata.attributes.filter(attr => attr.trait_type === 'Power'); 535 536 if (powers.length > 0) { 537 powers.forEach(power => { 538 const powerElement = document.createElement('li'); 539 powerElement.textContent = power.value; 540 modalPowers.appendChild(powerElement); 541 }); 542 } else { 543 const noPowersElement = document.createElement('li'); 544 noPowersElement.textContent = 'No special powers'; 545 modalPowers.appendChild(noPowersElement); 546 } 547 548 // Show modal 549 nftModal.style.display = 'block'; 550} 551 552// Close modal 553function closeNFTModal() { 554 nftModal.style.display = 'none'; 555} 556 557// Helper function to truncate text 558function truncateText(text, maxLength) { 559 if (!text) return ''; 560 561 if (text.length <= maxLength) return text; 562 563 return text.substring(0, maxLength) + '...'; 564} 565 566// Add background Ethereum symbols 567function addBackgroundSymbols() { 568 const heroSection = document.querySelector('.hero'); 569 const bgAnimation = document.createElement('div'); 570 bgAnimation.className = 'bg-animation'; 571 572 // Add Ethereum symbols 573 for (let i = 0; i < 20; i++) { 574 const symbol = document.createElement('div'); 575 symbol.className = 'eth-symbol'; 576 symbol.innerHTML = ` 577 <svg width="24" height="36" viewBox="0 0 24 36" fill="white" xmlns="http://www.w3.org/2000/svg"> 578 <path d="M11.9978 0L11.7783 0.741708V24.3193L11.9978 24.5387L23.9957 17.3821L11.9978 0Z" /> 579 <path d="M11.9979 0L0 17.3821L11.9979 24.5387V13.1195V0Z" /> 580 <path d="M11.9978 26.5754L11.8739 26.7262V35.1721L11.9978 35.5364L24.0001 19.4225L11.9978 26.5754Z" /> 581 <path d="M11.9979 35.5364V26.5754L0 19.4225L11.9979 35.5364Z" /> 582 <path d="M11.9978 24.5386L23.9956 17.382L11.9978 13.1194V24.5386Z" /> 583 <path d="M0 17.382L11.9979 24.5386V13.1194L0 17.382Z" /> 584 </svg> 585 `; 586 587 // Random positioning 588 const size = Math.random() * 30 + 10; // 10-40px 589 symbol.style.width = `${size}px`; 590 symbol.style.height = `${size * 1.5}px`; // Maintain aspect ratio 591 592 symbol.style.left = `${Math.random() * 100}%`; 593 symbol.style.bottom = `-${size}px`; 594 595 // Animation duration and delay 596 const duration = Math.random() * 15 + 10; // 10-25s 597 const delay = Math.random() * 15; 598 symbol.style.animationDuration = `${duration}s`; 599 symbol.style.animationDelay = `${delay}s`; 600 601 bgAnimation.appendChild(symbol); 602 } 603 604 heroSection.appendChild(bgAnimation); 605} 606 607// Event listeners 608document.addEventListener('DOMContentLoaded', () => { 609 // Initialize 610 initWeb3(); 611 612 // Add background animation
613 addBackgroundSymbols(); 614 615 // Modal close button 616 closeModal.addEventListener('click', closeNFTModal); 617 618 // Close modal when clicking outside 619 window.addEventListener('click', (event) => { 620 if (event.target === nftModal) { 621 closeNFTModal(); 622 } 623 }); 624 625 // Close modal with escape key 626 document.addEventListener('keydown', (event) => { 627 if (event.key === 'Escape') { 628 closeNFTModal(); 629 } 630 }); 631});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.