diff --git a/frontend/assets/js/app.js b/frontend/assets/js/app.js index 9ad37cd..7b35e7f 100644 --- a/frontend/assets/js/app.js +++ b/frontend/assets/js/app.js @@ -272,6 +272,10 @@ async function loadEvents() { } let allTags = []; +const PAGE_SIZE = 25; +let renderedCount = 0; +let filteredEvents = []; +let isLoadingMore = false; async function loadAllTags() { try { @@ -290,24 +294,36 @@ function renderTimeline() { const tagFilter = document.getElementById('tagFilter').value; const search = document.getElementById('searchEvents').value.toLowerCase(); - let filtered = events; - if (teamFilter) filtered = filtered.filter(e => e.team_id == teamFilter); - if (tagFilter) filtered = filtered.filter(e => e.tags && e.tags.includes(tagFilter)); - if (search) filtered = filtered.filter(e => + filteredEvents = events; + if (teamFilter) filteredEvents = filteredEvents.filter(e => e.team_id == teamFilter); + if (tagFilter) filteredEvents = filteredEvents.filter(e => e.tags && e.tags.includes(tagFilter)); + if (search) filteredEvents = filteredEvents.filter(e => e.title.toLowerCase().includes(search) || (e.description && e.description.toLowerCase().includes(search)) || (e.tags && e.tags.some(t => t.includes(search))) ); - if (!filtered.length) { + renderedCount = 0; + container.innerHTML = ''; + container.removeAttribute('style'); + renderMoreEvents(); +} + +function renderMoreEvents() { + const container = document.getElementById('timelineContainer'); + + if (!filteredEvents.length) { container.innerHTML = '

No events yet. Create your first incident entry!

'; return; } - container.innerHTML = filtered.map(e => { + const next = Math.min(renderedCount + PAGE_SIZE, filteredEvents.length); + const batch = filteredEvents.slice(renderedCount, next); + + const html = batch.map(e => { const date = new Date(e.occurred_at).toLocaleString(); return ` -
+
@@ -341,10 +357,44 @@ function renderTimeline() {
`; }).join(''); - filtered.forEach(e => { - const container = document.getElementById('attachments-' + e.id); - if (container) renderAttachments(e.id, container); + const wrapper = document.createElement('div'); + wrapper.innerHTML = html; + + const fragment = document.createDocumentFragment(); + while (wrapper.firstChild) fragment.appendChild(wrapper.firstChild); + container.appendChild(fragment); + + renderedCount = next; + + batch.forEach(e => { + const el = document.querySelector(`.timeline-item[data-event-id="${e.id}"] .attachments-list`); + if (el) renderAttachments(e.id, el); }); + + if (renderedCount < filteredEvents.length) { + const sentinel = document.createElement('div'); + sentinel.id = 'timeline-sentinel'; + sentinel.className = 'text-center py-3'; + sentinel.innerHTML = ''; + container.appendChild(sentinel); + observeSentinel(); + } +} + +let sentinelObserver = null; + +function observeSentinel() { + if (sentinelObserver) sentinelObserver.disconnect(); + const sentinel = document.getElementById('timeline-sentinel'); + if (!sentinel) return; + sentinelObserver = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && !isLoadingMore) { + isLoadingMore = true; + renderMoreEvents(); + isLoadingMore = false; + } + }, { rootMargin: '200px' }); + sentinelObserver.observe(sentinel); } function renderDocLinks(text) {