feat(web): add pagination and fix port binding for Tailscale access
- Paginate comic grid at 48 per page with smart page number controls - Bind container port to 0.0.0.0 so Tailscale traffic can reach WSL2
This commit is contained in:
@@ -2,7 +2,7 @@ services:
|
||||
yoink:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "0.0.0.0:8080:8080"
|
||||
volumes:
|
||||
- ./library:/library
|
||||
environment:
|
||||
|
||||
@@ -450,6 +450,44 @@
|
||||
text-shadow: 0 1px 4px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
/* ── Pagination ──────────────────────────────────────────────────────── */
|
||||
#pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
margin-top: 36px;
|
||||
padding-bottom: 36px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.page-btn {
|
||||
height: 34px;
|
||||
min-width: 34px;
|
||||
padding: 0 12px;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text2);
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.12s, color 0.12s, background 0.12s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.page-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--text); }
|
||||
.page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
.page-btn.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
.page-ellipsis {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
padding: 0 4px;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
/* ── Skeleton loading cards ──────────────────────────────────────────── */
|
||||
.skeleton-card {
|
||||
width: 150px;
|
||||
@@ -645,6 +683,7 @@
|
||||
</div>
|
||||
|
||||
<div id="comics-grid"></div>
|
||||
<div id="pagination"></div>
|
||||
|
||||
<div id="empty-state">
|
||||
<div class="empty-box">
|
||||
@@ -661,10 +700,12 @@
|
||||
|
||||
<script>
|
||||
// ── State ──────────────────────────────────────────────────────────────
|
||||
const PAGE_SIZE = 48;
|
||||
let knownJobs = {};
|
||||
let dismissedJobs = JSON.parse(localStorage.getItem('dismissedJobs') || '{}');
|
||||
let allComics = [];
|
||||
let currentSort = localStorage.getItem('comicSort') || 'newest';
|
||||
let currentPage = 1;
|
||||
|
||||
// ── DOM refs ───────────────────────────────────────────────────────────
|
||||
const form = document.getElementById('url-form');
|
||||
@@ -784,7 +825,7 @@
|
||||
}
|
||||
|
||||
// ── Filter & sort ──────────────────────────────────────────────────────
|
||||
function applyFilterAndSort() {
|
||||
function applyFilterAndSort(resetPage = false) {
|
||||
const query = filterInput.value.trim().toLowerCase();
|
||||
|
||||
let comics = query
|
||||
@@ -799,7 +840,56 @@
|
||||
comics = comics.slice().sort((a, b) => b.title.localeCompare(a.title));
|
||||
}
|
||||
|
||||
renderComics(comics);
|
||||
const totalPages = Math.max(1, Math.ceil(comics.length / PAGE_SIZE));
|
||||
if (resetPage || currentPage > totalPages) currentPage = 1;
|
||||
|
||||
const paged = comics.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
|
||||
renderComics(paged);
|
||||
renderPagination(totalPages);
|
||||
}
|
||||
|
||||
function renderPagination(totalPages) {
|
||||
const el = document.getElementById('pagination');
|
||||
el.innerHTML = '';
|
||||
if (totalPages <= 1) return;
|
||||
|
||||
const mkBtn = (label, page, active = false, disabled = false) => {
|
||||
const b = document.createElement('button');
|
||||
b.className = 'page-btn' + (active ? ' active' : '');
|
||||
b.textContent = label;
|
||||
b.disabled = disabled;
|
||||
b.addEventListener('click', () => {
|
||||
currentPage = page;
|
||||
applyFilterAndSort();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
return b;
|
||||
};
|
||||
|
||||
const mkEllipsis = () => {
|
||||
const s = document.createElement('span');
|
||||
s.className = 'page-ellipsis';
|
||||
s.textContent = '…';
|
||||
return s;
|
||||
};
|
||||
|
||||
el.append(mkBtn('← Prev', currentPage - 1, false, currentPage === 1));
|
||||
|
||||
// Page numbers with ellipsis: always show first, last, and window around current
|
||||
const pages = new Set([1, totalPages]);
|
||||
for (let p = currentPage - 2; p <= currentPage + 2; p++) {
|
||||
if (p > 1 && p < totalPages) pages.add(p);
|
||||
}
|
||||
const sorted = [...pages].sort((a, b) => a - b);
|
||||
|
||||
let prev = 0;
|
||||
for (const p of sorted) {
|
||||
if (p - prev > 1) el.append(mkEllipsis());
|
||||
el.append(mkBtn(p, p, p === currentPage));
|
||||
prev = p;
|
||||
}
|
||||
|
||||
el.append(mkBtn('Next →', currentPage + 1, false, currentPage === totalPages));
|
||||
}
|
||||
|
||||
// ── Render comics ──────────────────────────────────────────────────────
|
||||
@@ -921,11 +1011,11 @@
|
||||
currentSort = b.dataset.sort;
|
||||
localStorage.setItem('comicSort', currentSort);
|
||||
sortBtns.forEach(x => x.classList.toggle('active', x === b));
|
||||
applyFilterAndSort();
|
||||
applyFilterAndSort(true);
|
||||
});
|
||||
});
|
||||
|
||||
filterInput.addEventListener('input', applyFilterAndSort);
|
||||
filterInput.addEventListener('input', () => applyFilterAndSort(true));
|
||||
|
||||
showSkeletons();
|
||||
fetchComics();
|
||||
|
||||
Reference in New Issue
Block a user