PageSourceSearch

https://www.vybri.ai/js/resources.js

js vybri.ai collected 2026-10-05 15:58:16 UTC 2,808 bytes, 102 lines download raw bytes

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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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.