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 = `
+
+ `;
+}
+
+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.id)}
+ #${highlight(p.id, query)}
${esc(relTime(p.date))}
${tags}
- ${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);