1(function() { 2 const listEl = document.getElementById('resources-list'); 3 const loadingEl = document.getElementById('loading'); 4 const sentinel = document.getElementById('sentinel'); 5 const filters = document.getElementById('resource-filters'); 6 7 let page = 0; 8 const pageSize = 9; 9 let allItems = []; 10 let activeFilter = 'all'; 11 let loading = false; 12 let done = false; 13 14 function renderCard(item) { 15 const card = document.createElement('article'); 16 card.className = 'resource-card'; 17 card.innerHTML = ` 18 <div class="type">${item.type}</div> 19 <h3>${escapeHtml(item.title)}</h3> 20 <p>${escapeHtml(item.summary)}</p> 21 <a class="read-more" href="${item.url}" target="_blank" rel="noopener noreferrer">Open in new tab â</a> 22 `; 23 return card; 24 } 25 26 function escapeHtml(str) { 27 return str.replace(/[&<>"]+/g, s => ({'&':'&','<':'<','>':'>','"':'"'}[s])); 28 } 29 30 function applyFilter(items, filter) { 31 if (filter === 'all') return items; 32 return items.filter(i => i.type === filter); 33 } 34 35 function showLoading(show) { 36 loadingEl.hidden = !show; 37 } 38 39 async function loadData() { 40 if (allItems.length) return allItems; 41 const res = await fetch('resources.json'); 42 if (!res.ok) throw new Error('Failed to load resources.json'); 43 const data = await res.json(); 44 allItems = data.items || []; 45 return allItems; 46 } 47 48 function nextPageItems() { 49 const filtered = applyFilter(allItems, activeFilter); 50 const start = page * pageSize; 51 const slice = filtered.slice(start, start + pageSize); 52 page += 1; 53 if (start + slice.length >= filtered.length) done = true; 54 return slice; 55 } 56 57 async function loadMore() { 58 if (loading || done) return; 59 loading = true; 60 showLoading(true); 61 try { 62 await loadData(); 63 const items = nextPageItems(); 64 for (const item of items) listEl.appendChild(renderCard(item)); 65 } catch (e) { 66 console.error(e); 67 } finally { 68 showLoading(false); 69 loading = false; 70 } 71 } 72 73 function resetAndRender() { 74 page = 0; 75 done = false; 76 listEl.innerHTML = ''; 77 loadMore(); 78 } 79 80 // Filters 81 filters.addEventListener('click', (e) => { 82 const btn = e.target.closest('.chip'); 83 if (!btn) return; 84 [...filters.querySelectorAll('.chip')].forEach(b => { 85 b.classList.toggle('active', b === btn); 86 b.setAttribute('aria-pressed', b === btn ? 'true' : 'false'); 87 });
88 activeFilter = btn.dataset.filter; 89 resetAndRender(); 90 }); 91 92 // Infinite scroll via IntersectionObserver 93 const io = new IntersectionObserver(entries => { 94 entries.forEach(entry => { 95 if (entry.isIntersecting) loadMore(); 96 }); 97 }, { rootMargin: '600px 0px' }); 98 io.observe(sentinel); 99 100 // Initial render 101 loadMore(); 102})();
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.