feat: Implement infinite scroll for the tags page by adding a paginated API endpoint and client-side loading logic.

This commit is contained in:
x
2026-01-24 19:28:51 +01:00
parent 7896e6983f
commit 87e6e5355a
2 changed files with 130 additions and 11 deletions

View File

@@ -1,11 +1,53 @@
import db from "../../inc/sql.mjs";
import cfg from "../../inc/config.mjs";
const TAGS_PER_PAGE = 50;
export default (router, tpl) => {
// API endpoint for lazy loading tags
router.get(/^\/api\/tags$/, async (req, res) => {
const page = Math.max(1, +(req.url.qs?.page ?? 1));
const offset = (page - 1) * TAGS_PER_PAGE;
const isLoggedIn = !!req.session;
const nsfp = cfg.nsfp.map(n => `${n}`);
let tags;
if (isLoggedIn) {
tags = await db`
SELECT t.id, t.tag, COUNT(DISTINCT ta.item_id) AS total_items
FROM tags t
LEFT JOIN tags_assign ta ON t.id = ta.tag_id
GROUP BY t.id, t.tag
HAVING COUNT(DISTINCT ta.item_id) >= 1
ORDER BY total_items DESC
OFFSET ${offset}
LIMIT ${TAGS_PER_PAGE}
`;
} else {
tags = await db`
SELECT t.id, t.tag, COUNT(DISTINCT ta.item_id) AS total_items
FROM tags t
LEFT JOIN tags_assign ta ON t.id = ta.tag_id
WHERE t.id not in (${db.unsafe(nsfp)})
GROUP BY t.id, t.tag
HAVING COUNT(DISTINCT ta.item_id) >= 1
ORDER BY total_items DESC
OFFSET ${offset}
LIMIT ${TAGS_PER_PAGE}
`;
}
res.json({
tags,
page,
hasMore: tags.length === TAGS_PER_PAGE
});
});
// Main tags page - only load first page
router.get(/^\/tags$/, async (req, res) => {
const phrase = cfg.websrv.phrases[~~(Math.random() * cfg.websrv.phrases.length)];
const nsfp = cfg.nsfp.map(n => `${n}`);
const toptags = await db`
@@ -16,7 +58,7 @@ export default (router, tpl) => {
GROUP BY t.id, t.tag
HAVING COUNT(DISTINCT ta.item_id) >= 1
ORDER BY total_items DESC
;
LIMIT ${TAGS_PER_PAGE}
`;
const toptags_regged = await db`
@@ -26,7 +68,7 @@ export default (router, tpl) => {
GROUP BY t.id, t.tag
HAVING COUNT(DISTINCT ta.item_id) >= 1
ORDER BY total_items DESC
;
LIMIT ${TAGS_PER_PAGE}
`;
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
@@ -43,5 +85,6 @@ export default (router, tpl) => {
}, req)
});
});
return router;
};

View File

@@ -2,33 +2,109 @@
<div id="main">
<div class="container">
<h3 style="text-align: center;"></h3>
<div class="tags-grid">
<div class="tags-grid" id="tags-container">
@if(session)
@each(toptags_regged as toptag)
<a href="/tag/{!! toptag.tag !!}" class="tag-card">
<a href="/tag/{{ toptag.tag }}" class="tag-card">
<div class="tag-card-image">
<img src="/tag_image/{!! toptag.tag !!}" loading="lazy" alt="{!! toptag.tag !!}">
<img src="/tag_image/{{ toptag.tag }}" loading="lazy" alt="{{ toptag.tag }}">
</div>
<div class="tag-card-content">
<span class="tag-name">#{!! toptag.tag !!}</span>
<span class="tag-name">#{{ toptag.tag }}</span>
<span class="tag-count">{{ toptag.total_items }} posts</span>
</div>
</a>
@endeach
@else
@each(toptags as toptag)
<a href="/tag/{!! toptag.tag !!}" class="tag-card">
<a href="/tag/{{ toptag.tag }}" class="tag-card">
<div class="tag-card-image">
<img src="/tag_image/{!! toptag.tag !!}" loading="lazy" alt="{!! toptag.tag !!}">
<img src="/tag_image/{{ toptag.tag }}" loading="lazy" alt="{{ toptag.tag }}">
</div>
<div class="tag-card-content">
<span class="tag-name">#{!! toptag.tag !!}</span>
<span class="tag-name">#{{ toptag.tag }}</span>
<span class="tag-count">{{ toptag.total_items }} posts</span>
</div>
</a>
@endeach
@endif
</div>
<div id="tags-loader" style="text-align: center; padding: 20px; display: none;">
<span style="color: #888;">Loading more tags...</span>
</div>
<div id="tags-end" style="text-align: center; padding: 20px; display: none;">
<span style="color: #666;">No more tags</span>
</div>
</div>
</div>
<script>
(function () {
let page = 1;
let loading = false;
let hasMore = true;
const container = document.getElementById('tags-container');
const loader = document.getElementById('tags-loader');
const endMsg = document.getElementById('tags-end');
const escapeHtml = (str) => {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
};
const loadMore = async () => {
if (loading || !hasMore) return;
loading = true;
loader.style.display = 'block';
page++;
try {
const res = await fetch(`/api/tags?page=${page}`);
const data = await res.json();
if (!data.tags || data.tags.length === 0) {
hasMore = false;
loader.style.display = 'none';
endMsg.style.display = 'block';
return;
}
hasMore = data.hasMore;
data.tags.forEach(tag => {
const escapedTag = escapeHtml(tag.tag);
const html = `
<a href="/tag/${encodeURIComponent(tag.tag)}" class="tag-card">
<div class="tag-card-image">
<img src="/tag_image/${encodeURIComponent(tag.tag)}" loading="lazy" alt="${escapedTag}">
</div>
<div class="tag-card-content">
<span class="tag-name">#${escapedTag}</span>
<span class="tag-count">${tag.total_items} posts</span>
</div>
</a>
`;
container.insertAdjacentHTML('beforeend', html);
});
if (!hasMore) {
endMsg.style.display = 'block';
}
} catch (e) {
console.error('Failed to load tags:', e);
} finally {
loading = false;
loader.style.display = 'none';
}
};
// Infinite scroll
window.addEventListener('scroll', () => {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 500) {
loadMore();
}
});
})();
</script>
@include(snippets/footer)