1// =================================== 2// Web3 Graveyard - Main JavaScript 3// Infinite Scroll + Brutalist Design 4// =================================== 5 6// State Management 7const state = { 8 projects: [], 9 filteredProjects: [], 10 displayedCount: 0, 11 itemsPerBatch: 9, 12 isLoading: false, 13 currentCategory: 'all', 14 searchQuery: '' 15}; 16 17// DOM Elements 18const elements = { 19 searchInput: document.getElementById('searchInput'), 20 resultCount: document.getElementById('resultCount'), 21 categoryFilters: document.getElementById('categoryFilters'), 22 projectsGrid: document.getElementById('projectsGrid'), 23 loadingIndicator: document.getElementById('loadingIndicator'), 24 modalOverlay: document.getElementById('modalOverlay'), 25 modalContent: document.getElementById('modalContent'), 26 modalClose: document.getElementById('modalClose') 27}; 28 29// Initialize Application 30function init() { 31 if (typeof cryptoGraveyardData !== 'undefined') { 32 state.projects = cryptoGraveyardData.projects; 33 state.filteredProjects = [...state.projects]; 34 35 // Load initial batch 36 loadMoreProjects(); 37 38 // Setup event listeners 39 setupEventListeners(); 40 setupInfiniteScroll(); 41 } else { 42 console.error('Data not loaded'); 43 } 44} 45 46// Setup Event Listeners 47function setupEventListeners() { 48 // Search functionality 49 elements.searchInput.addEventListener('input', debounce(handleSearch, 300)); 50 51 // Category filter buttons 52 elements.categoryFilters.addEventListener('click', (e) => { 53 const btn = e.target.closest('.filter-btn'); 54 if (btn) { 55 handleCategoryFilter(btn); 56 } 57 }); 58 59 // Modal functionality 60 elements.modalClose.addEventListener('click', closeModal); 61 elements.modalOverlay.addEventListener('click', (e) => { 62 if (e.target === elements.modalOverlay) closeModal(); 63 }); 64 65 // Keyboard navigation 66 document.addEventListener('keydown', (e) => { 67 if (e.key === 'Escape') closeModal(); 68 }); 69 70 // Smooth scroll for hero buttons 71 const exploreBtn = document.getElementById('exploreBtn'); 72 const lessonsBtn = document.getElementById('lessonsBtn'); 73 74 const scrollToContent = (e) => { 75 e.preventDefault(); 76 const target = document.getElementById('searchSection'); 77 if (target) { 78 target.scrollIntoView({ behavior: 'smooth', block: 'start' }); 79 } 80 }; 81 82 if (exploreBtn) { 83 exploreBtn.addEventListener('click', scrollToContent); 84 } 85 86 if (lessonsBtn) { 87 lessonsBtn.addEventListener('click', scrollToContent); 88 } 89} 90 91// Setup Infinite Scroll 92function setupInfiniteScroll() { 93 const observer = new IntersectionObserver((entries) => { 94 entries.forEach(entry => { 95 if (entry.isIntersecting && !state.isLoading) { 96 loadMoreProjects(); 97 } 98 }); 99 }, { 100 rootMargin: '200px' 101 }); 102 103 observer.observe(elements.loadingIndicator); 104} 105 106// Debounce Function 107function debounce(func, wait) { 108 let timeout; 109 return function executedFunction(...args) { 110 const later = () => { 111 clearTimeout(timeout); 112 func(...args); 113 }; 114 clearTimeout(timeout); 115 timeout = setTimeout(later, wait); 116 }; 117} 118 119// Handle Search 120function handleSearch(e) { 121 state.searchQuery = e.target.value.toLowerCase().trim(); 122 resetAndFilter(); 123} 124 125// Handle Category Filter 126function handleCategoryFilter(button) { 127 // Update active state 128 document.querySelectorAll('.filter-btn').forEach(btn => { 129 btn.classList.remove('active'); 130 }); 131 button.classList.add('active'); 132 133 state.currentCategory = button.dataset.filter; 134 resetAndFilter(); 135} 136 137// Reset and Filter 138function resetAndFilter() { 139 state.displayedCount = 0; 140 elements.projectsGrid.innerHTML = ''; 141 142 // Apply filters 143 state.filteredProjects = state.projects.filter(project => { 144 // Search filter 145 const matchesSearch = state.searchQuery === '' || 146 project.name.toLowerCase().includes(state.searchQuery) || 147 project.description.toLowerCase().includes(state.searchQuery) || 148 project.category.toLowerCase().includes(state.searchQuery) || 149 project.failureReasons.some(r => r.toLowerCase().includes(state.searchQuery)); 150 151 // Category filter 152 const matchesCategory = state.currentCategory === 'all' || 153 project.category === state.currentCategory; 154 155 return matchesSearch && matchesCategory; 156 }); 157 158 // Sort by losses (highest first) 159 state.filteredProjects.sort((a, b) => { 160 return parseLoss(b.estimatedLosses) - parseLoss(a.estimatedLosses); 161 }); 162 163 updateResultCount();
164 loadMoreProjects(); 165} 166 167// Parse Loss Value 168function parseLoss(lossString) { 169 if (!lossString || lossString === 'N/A' || lossString === 'Undisclosed') return 0; 170 171 const match = lossString.match(/\$?([\d.]+)\s*(billion|million|B|M)?/i); 172 if (!match) return 0; 173 174 let value = parseFloat(match[1]); 175 const unit = (match[2] || '').toLowerCase(); 176 177 if (unit === 'billion' || unit === 'b') value *= 1000000000; 178 else if (unit === 'million' || unit === 'm') value *= 1000000; 179 180 return value; 181} 182 183// Load More Projects 184function loadMoreProjects() { 185 if (state.isLoading) return; 186 if (state.displayedCount >= state.filteredProjects.length) { 187 elements.loadingIndicator.classList.remove('visible'); 188 return; 189 } 190 191 state.isLoading = true; 192 elements.loadingIndicator.classList.add('visible'); 193 194 // Simulate loading delay for effect 195 setTimeout(() => { 196 const start = state.displayedCount; 197 const end = Math.min(start + state.itemsPerBatch, state.filteredProjects.length); 198 const projectsToAdd = state.filteredProjects.slice(start, end); 199 200 projectsToAdd.forEach(project => { 201 const card = createProjectCard(project); 202 elements.projectsGrid.appendChild(card); 203 }); 204 205 state.displayedCount = end; 206 state.isLoading = false; 207 208 if (state.displayedCount >= state.filteredProjects.length) { 209 elements.loadingIndicator.classList.remove('visible'); 210 } 211 }, 300); 212} 213 214// Create Project Card 215function createProjectCard(project) { 216 const card = document.createElement('article'); 217 card.className = 'project-card'; 218 card.dataset.id = project.id; 219 220 const year = project.shutdownDate ? new Date(project.shutdownDate).getFullYear() : 'N/A'; 221 const burned = formatBurned(project.estimatedLosses); 222 const ratings = generateRatings(project); 223 224 // SVG Icons 225 const tagIcon = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>'; 226 const skullIcon = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12c0 3.69 2.47 6.86 6 8.25V22h8v-1.75c3.53-1.39 6-4.56 6-8.25 0-5.52-4.48-10-10-10zm-2 16h-1v-2h1v2zm6 0h-1v-2h1v2zm1-5.5c0 .28-.22.5-.5.5h-9c-.28 0-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5H8V9c0-2.21 1.79-4 4-4s4 1.79 4 4v2h.5c.28 0 .5.22.5.5v1z"/></svg>'; 227 const wrenchIcon = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"/></svg>'; 228 const chartIcon = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/></svg>'; 229 const targetIcon = '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm-5-9h4V7h2v4h4v2h-4v4h-2v-4H7z"/></svg>'; 230 231 card.innerHTML = ` 232 <div class="card-header"> 233 <div class="card-top"> 234 <h3 class="card-name">${project.name}</h3> 235 <span class="card-year">${tagIcon}${year}</span> 236 </div> 237 <p class="card-description">${project.description}</p> 238 </div> 239 <div class="card-body"> 240 <div class="card-meta"> 241 <span class="card-burned">${burned} BURNED</span> 242 <span class="card-category">${skullIcon}${formatCategory(project.category)}</span> 243 </div> 244 <div class="card-hover-text">HOVER FOR DETAILS</div> 245 </div> 246 <div class="card-footer"> 247 <div class="card-ratings"> 248 <div class="rating-item"> 249 <span class="rating-label rebuild">${wrenchIcon}REBUILD</span> 250 <div class="rating-bars">${createRatingBars(ratings.rebuild)}</div> 251 </div> 252 <div class="rating-item">
253 <span class="rating-label scale">${chartIcon}SCALE</span> 254 <div class="rating-bars">${createRatingBars(ratings.scale)}</div> 255 </div> 256 <div class="rating-item"> 257 <span class="rating-label market">${targetIcon}MARKET</span> 258 <div class="rating-bars">${createRatingBars(ratings.market)}</div> 259 </div> 260 </div> 261 </div> 262 `; 263 264 card.addEventListener('click', () => openModal(project)); 265 return card; 266} 267 268// Format Burned Amount 269function formatBurned(losses) { 270 if (!losses || losses === 'N/A' || losses === 'Undisclosed') return '???'; 271 272 const match = losses.match(/\$?([\d.]+)\s*(billion|million|B|M|\+)?/i); 273 if (!match) return '???'; 274 275 const value = parseFloat(match[1]); 276 const unit = (match[2] || '').toLowerCase(); 277 278 if (unit === 'billion' || unit === 'b' || unit === 'billion+') { 279 return `$${value}B`; 280 } else if (unit === 'million' || unit === 'm' || unit === 'million+') { 281 return `$${value}M`; 282 } else if (value >= 1000) { 283 return `$${(value / 1000).toFixed(1)}B`; 284 } 285 return `$${value}M`; 286} 287 288// Format Category 289function formatCategory(category) { 290 const categoryMap = { 291 'Exchange': 'EXCHANGE', 292 'DeFi': 'DEFI', 293 'NFT': 'NFT', 294 'Stablecoin': 'STABLECOIN', 295 'CryptoLender': 'LENDER', 296 'Bridge': 'BRIDGE', 297 'GameFi': 'GAMEFI', 298 'RugPull': 'RUG PULL', 299 'Infrastructure': 'INFRA', 300 'Layer1Blockchain': 'LAYER 1' 301 }; 302 return categoryMap[category] || category.toUpperCase(); 303} 304 305// Generate Ratings (pseudo-ratings based on project data) 306function generateRatings(project) { 307 // Generate ratings based on failure type and losses 308 const isRugPull = project.category === 'RugPull' || 309 project.failureReasons.some(r => r.toLowerCase().includes('rug') || r.toLowerCase().includes('fraud') || r.toLowerCase().includes('ponzi')); 310 311 const hasRecovery = project.legalOutcome && 312 (project.legalOutcome.toLowerCase().includes('return') || 313 project.legalOutcome.toLowerCase().includes('recover') || 314 project.legalOutcome.toLowerCase().includes('refund')); 315 316 const loss = parseLoss(project.estimatedLosses); 317 const lossScore = loss > 1000000000 ? 1 : loss > 100000000 ? 2 : loss > 10000000 ? 3 : 4; 318 319 return { 320 rebuild: isRugPull ? 1 : (hasRecovery ? 4 : 2 + Math.floor(Math.random() * 2)), 321 scale: Math.min(5, lossScore + Math.floor(Math.random() * 2)), 322 market: isRugPull ? 1 : 2 + Math.floor(Math.random() * 3) 323 }; 324} 325 326// Create Rating Bars 327function createRatingBars(rating) { 328 let bars = ''; 329 for (let i = 0; i < 5; i++) { 330 if (i < rating) { 331 bars += '<div class="rating-bar filled"></div>'; 332 } else { 333 bars += '<div class="rating-bar"></div>'; 334 } 335 } 336 return bars; 337} 338 339// Update Result Count 340function updateResultCount() { 341 const count = state.filteredProjects.length; 342 elements.resultCount.textContent = `${count} CORPSE${count !== 1 ? 'S' : ''} FOUND`; 343} 344 345// Format Date 346function formatDate(dateString) { 347 if (!dateString) return 'N/A'; 348 const date = new Date(dateString); 349 return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); 350} 351 352// Open Modal 353function openModal(project) { 354 elements.modalContent.innerHTML = createModalContent(project); 355 elements.modalOverlay.classList.add('visible'); 356 document.body.style.overflow = 'hidden'; 357} 358 359// Close Modal 360function closeModal() { 361 elements.modalOverlay.classList.remove('visible'); 362 document.body.style.overflow = ''; 363} 364 365// Create Modal Content 366function createModalContent(project) { 367 return ` 368 <div class="modal-header"> 369 <span class="modal-category-badge">${formatCategory(project.category)}</span> 370 <h2 class="modal-title">${project.name}</h2> 371 <p class="modal-dates">${formatDate(project.launchDate)} â ${formatDate(project.shutdownDate)}</p> 372 </div> 373 <div class="modal-body"> 374 <div class="modal-section"> 375 <h4 class="modal-section-title">WHAT HAPPENED</h4> 376 <p class="modal-section-content">${project.description}</p> 377 </div> 378 379 <div class="modal-section"> 380 <h4 class="modal-section-title">THE DAMAGE</h4> 381 <div class="modal-stats-grid"> 382 <div class="modal-stat"> 383 <div class="modal-stat-label">ESTIMATED LOSSES</div> 384 <div class="modal-stat-value loss">${project.estimatedLosses || 'N/A'}</div> 385 </div> 386 <div class="modal-stat"> 387 <div class="modal-stat-label">PEAK VALUATION</div> 388 <div class="modal-stat-value">${project.peakValuation || 'N/A'}</div> 389 </div> 390 <div class="modal-stat"> 391 <div class="modal-stat-label">FUNDING RAISED</div> 392 <div class="modal-stat-value">
392${project.fundingRaised || 'N/A'}</div> 393 </div> 394 <div class="modal-stat"> 395 <div class="modal-stat-label">AFFECTED USERS</div> 396 <div class="modal-stat-value">${project.affectedUsers || 'N/A'}</div> 397 </div> 398 </div> 399 </div> 400 401 <div class="modal-section"> 402 <h4 class="modal-section-title">CAUSE OF DEATH</h4> 403 <div class="failure-reasons"> 404 ${project.failureReasons.map(reason => 405 `<span class="failure-reason">${reason}</span>` 406 ).join('')} 407 </div> 408 </div> 409 410 <div class="modal-section"> 411 <h4 class="modal-section-title">LEGAL OUTCOME</h4> 412 <div class="modal-legal"> 413 ${project.legalOutcome || 'No legal action taken or outcome unknown.'} 414 </div> 415 </div> 416 417 ${project.revivalIdeas && 418 project.revivalIdeas[0] !== 'N/A - was never a legitimate project' && 419 project.revivalIdeas[0] !== 'N/A - was allegedly fraudulent from inception' && 420 project.revivalIdeas[0] !== 'N/A - company wound down' && 421 project.revivalIdeas[0] !== 'N/A - was fraudulent from inception' ? ` 422 <div class="modal-section"> 423 <h4 class="modal-section-title">REVIVAL IDEAS</h4> 424 <ul class="revival-ideas"> 425 ${project.revivalIdeas.map(idea => 426 `<li><svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"/></svg>${idea}</li>` 427 ).join('')} 428 </ul> 429 </div> 430 ` : ''} 431 </div> 432 `; 433} 434 435// Initialize when DOM is ready 436document.addEventListener('DOMContentLoaded', init);
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.