From 6e96e34c0e525f3fee99ed1d69b46beab9dfacce Mon Sep 17 00:00:00 2001 From: Ty Clifford Date: Sun, 7 Jun 2026 12:06:03 -0400 Subject: [PATCH] - Search, pagination --- post.php | 311 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 297 insertions(+), 14 deletions(-) diff --git a/post.php b/post.php index cae015e..4caf6ab 100644 --- a/post.php +++ b/post.php @@ -404,6 +404,116 @@ define('POST_PAGE_TITLE', 'Updates — Post Manager'); .edit-form.open { display: flex; animation: fade-in .2s ease; } /* ── Misc ───────────────────────────────────────────────────────────────── */ + /* ── Search bar ─────────────────────────────────────────────────────────── */ + .search-wrap { + padding: .65rem 1.25rem; + border-bottom: 1px solid var(--border); + position: relative; + } + .search-input-wrap { + position: relative; + display: flex; + align-items: center; + } + .search-icon { + position: absolute; + left: .75rem; + color: var(--text-3); + pointer-events: none; + flex-shrink: 0; + } + .search-input { + width: 100%; + padding: .55rem .9rem .55rem 2.2rem; + font-family: 'Share Tech Mono', monospace; + font-size: .82rem; + color: var(--text); + background: rgba(255,255,255,.03); + border: 1px solid var(--border-2); + border-radius: var(--r); + outline: none; + transition: border-color .2s, box-shadow .2s, background .2s; + } + .search-input:focus { + border-color: var(--blue); + background: rgba(0,200,255,.04); + box-shadow: 0 0 0 3px rgba(0,200,255,.1); + } + .search-input::placeholder { color: var(--text-3); } + .search-clear { + position: absolute; + right: .6rem; + background: none; + border: none; + cursor: pointer; + color: var(--text-3); + padding: .2rem; + display: none; + align-items: center; + transition: color .15s; + } + .search-clear:hover { color: var(--red); } + .search-clear.visible { display: flex; } + /* highlight matched text */ + mark { + background: rgba(0,200,255,.2); + color: var(--blue); + border-radius: 2px; + padding: 0 .1em; + } + + /* ── Pagination ──────────────────────────────────────────────────────────── */ + .pagination-bar { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: .5rem; + padding: .65rem 1.25rem; + border-top: 1px solid var(--border); + } + .pagination-info { + font-family: 'Share Tech Mono', monospace; + font-size: .6rem; + letter-spacing: .06em; + color: var(--text-3); + } + .pagination-btns { + display: flex; + align-items: center; + gap: .3rem; + flex-wrap: wrap; + } + .page-btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 28px; + height: 28px; + padding: 0 .45rem; + border: 1px solid var(--border-2); + border-radius: 4px; + background: rgba(255,255,255,.03); + font-family: 'Share Tech Mono', monospace; + font-size: .65rem; + letter-spacing: .04em; + color: var(--text-3); + cursor: pointer; + transition: border-color .15s, color .15s, background .15s; + } + .page-btn:hover:not(:disabled) { + border-color: var(--green); + color: var(--green); + background: rgba(0,232,122,.05); + } + .page-btn.active { + border-color: var(--green); + color: var(--green); + background: rgba(0,232,122,.08); + } + .page-btn:disabled { opacity: .3; cursor: not-allowed; } + .page-ellipsis { border-color: transparent; background: transparent; cursor: default; pointer-events: none; } + .feed-status { padding: 2.5rem 1.25rem; font-family: 'Share Tech Mono', monospace; font-size: .72rem; @@ -540,9 +650,34 @@ define('POST_PAGE_TITLE', 'Updates — Post Manager'); All Posts + + +
+
+ + + +
+
+ + + + @@ -760,35 +895,179 @@ function doAuth() { function doLogout() { sessionStorage.removeItem(STORE_KEY); location.reload(); } -/* ── Feed ────────────────────────────────────────────────────────────────────── */ +/* ════════════════════════════════════════════════════════════════════════════ + FEED — search, pagination, rendering +════════════════════════════════════════════════════════════════════════════ */ +const PER_PAGE = 15; // posts shown per page +let allPosts = []; // master array from last API fetch +let currentPage = 1; +let searchQuery = ''; +let searchTimer = null; + +/* Load all posts from API, then filter+render */ function loadFeed() { const list = document.getElementById('post-list'); list.innerHTML = `
  • loading…
  • `; - apiFetch('') - .then(data => renderFeed(data.items, data.total)) - .catch(err => { list.innerHTML = `
  • failed: ${esc(err.message)}
  • `; }); + apiFetch('limit=1000') // fetch everything; client handles paging + .then(data => { + allPosts = data.items || []; + currentPage = 1; + filterAndPage(); + }) + .catch(err => { + list.innerHTML = `
  • failed: ${esc(err.message)}
  • `; + hidePagination(); + }); +} + +/* Filter allPosts by searchQuery, then render the requested page */ +function filterAndPage() { + const q = searchQuery.trim().toLowerCase(); + + const filtered = q + ? allPosts.filter(p => + (p.text || '').toLowerCase().includes(q) || + (p.id || '').toLowerCase().includes(q) || + (p.tags || []).some(t => t.toLowerCase().includes(q)) || + (p.link || '').toLowerCase().includes(q) || + (p.note || '').toLowerCase().includes(q) + ) + : allPosts; + + const total = filtered.length; + const totalPages = Math.max(1, Math.ceil(total / PER_PAGE)); + currentPage = Math.min(currentPage, totalPages); + const start = (currentPage - 1) * PER_PAGE; + const page = filtered.slice(start, start + PER_PAGE); + + // Update badge + const badge = document.getElementById('post-count'); + if (q) { + badge.textContent = filtered.length + ' of ' + allPosts.length + ' match' + (filtered.length !== 1 ? 'es' : ''); + } else { + badge.textContent = allPosts.length + ' post' + (allPosts.length !== 1 ? 's' : ''); + } + + // Render list + const list = document.getElementById('post-list'); + if (!page.length) { + list.innerHTML = q + ? `
  • no results for "${esc(q)}"
  • ` + : '
  • no posts yet
  • '; + hidePagination(); + return; + } + list.innerHTML = page.map(p => postHTML(p, q)).join(''); + + // Render pagination + if (totalPages <= 1) { + hidePagination(); + } else { + renderPagination(currentPage, totalPages, total, start); + } +} + +/* ── Pagination controls ─────────────────────────────────────────────────── */ +function renderPagination(page, totalPages, total, start) { + const bar = document.getElementById('pagination-bar'); + bar.style.display = ''; + bar.className = 'pagination-bar'; + + const from = start + 1; + const to = Math.min(start + PER_PAGE, total); + + // Build page number buttons with sliding window ± 2 + let btns = ''; + const lo = Math.max(1, page - 2); + const hi = Math.min(totalPages, page + 2); + + btns += ``; + + if (lo > 1) { + btns += ``; + if (lo > 2) btns += ``; + } + for (let p = lo; p <= hi; p++) { + btns += ``; + } + if (hi < totalPages) { + if (hi < totalPages - 1) btns += ``; + btns += ``; + } + btns += ``; + + bar.innerHTML = ` + ${from}–${to} of ${total} +
    ${btns}
    `; +} + +function hidePagination() { + const bar = document.getElementById('pagination-bar'); + bar.style.display = 'none'; + bar.innerHTML = ''; +} + +function goPage(n) { + currentPage = n; + filterAndPage(); + // Scroll the list back into view smoothly + document.getElementById('post-list').scrollIntoView({ behavior: 'smooth', block: 'start' }); +} + +/* ── Search input handler (debounced 250 ms) ─────────────────────────────── */ +function onSearchInput(val) { + const clearBtn = document.getElementById('search-clear'); + clearBtn.classList.toggle('visible', val.length > 0); + clearTimeout(searchTimer); + searchTimer = setTimeout(() => { + searchQuery = val; + currentPage = 1; + filterAndPage(); + }, 250); +} + +function clearSearch() { + document.getElementById('search-input').value = ''; + document.getElementById('search-clear').classList.remove('visible'); + searchQuery = ''; + currentPage = 1; + filterAndPage(); + document.getElementById('search-input').focus(); +} + +/* ── Highlight matched text ──────────────────────────────────────────────── */ +function highlight(text, query) { + if (!query) return esc(text); + var safe = esc(text); + // Escape regex special chars safely without regex literal char-class issues + var safeQ = esc(query).replace(/[-[\]{}()*+?.,^$|#\s]/g, '\\$&'); + try { + return safe.replace(new RegExp('(' + safeQ + ')', 'gi'), '$1'); + } catch(e) { return safe; } } function renderFeed(posts, total) { - document.getElementById('post-count').textContent = total + ' post' + (total!==1?'s':''); - const list = document.getElementById('post-list'); - list.innerHTML = posts.length - ? posts.map(p => postHTML(p)).join('') - : '
  • no posts yet
  • '; + // Legacy path — called by old code paths if any; route through new system + allPosts = posts; + filterAndPage(); } -function postHTML(p) { - const tags = (p.tags||[]).map(t=>`${esc(t)}`).join(''); +function postHTML(p, query) { + query = query || ''; + const tags = (p.tags||[]).map(t=>`${highlight(t, query)}`).join(''); const link = p.link ? `${esc(p.link_label||p.link)}` : ''; const media = mediaStripHTML(p.media||[]); return `
  • -

    ${esc(p.text)}

    +

    ${highlight(p.text, query)}

    ${link} ${media}
    @@ -911,7 +1190,7 @@ function saveEdit(id) { return apiFetch('id=' + encodeURIComponent(id), { method: 'PUT', body: JSON.stringify(body) }); }) .then(updated => { - document.getElementById('pi-' + id).outerHTML = postHTML(updated); + document.getElementById('pi-' + id).outerHTML = postHTML(updated, searchQuery); }) .catch(err => showAlert('ef-alert-'+id,'error',err.message)); } @@ -931,6 +1210,10 @@ function deletePost(id) { /* ── Enter key + session restore ─────────────────────────────────────────────── */ document.getElementById('key-input').addEventListener('keydown', e => { if (e.key==='Enter') doAuth(); }); +/* Search: clear on Escape */ +document.addEventListener('keydown', e => { + if (e.key === 'Escape' && searchQuery) clearSearch(); +}); (function() { const saved = sessionStorage.getItem(STORE_KEY);