refactor: update card grid filtering and pagination logic to support category/tag filters and display empty states

This commit is contained in:
2026-08-03 20:34:15 +07:00
parent 141d3a71fd
commit 417f6ecb24
5 changed files with 90 additions and 30 deletions
@@ -188,13 +188,22 @@
<button type="button" class="tab-btn active" data-filter="ALL">Tất cả</button>
</div>
<!-- 2. CARD GRID -->
<div class="news-grid" th:data-items-per-page="${block.data['itemsPerPage'] != null ? block.data['itemsPerPage'] : (block.data['limit'] != null ? block.data['limit'] : 9)}">
<th:block th:each="p : ${posts}" th:if="${(block.data['layoutFilter'] == null or block.data['layoutFilter'] == 'ALL' or (p.layout != null and #strings.equals(p.layout.name(), block.data['layoutFilter'])))}">
<!-- 2. CARD GRID (Strictly capped at 12 items per page max) -->
<div class="news-grid" th:data-items-per-page="${block.data['limit'] != null and block.data['limit'] > 0 ? block.data['limit'] : 12}">
<th:block th:each="p : ${posts}"
th:if="${(
(block.data['categoryFilter'] == null or #strings.isEmpty(block.data['categoryFilter']) or #strings.contains(block.data['categoryFilter'], 'ALL'))
and
(block.data['tagFilter'] == null or #strings.isEmpty(block.data['tagFilter']) or #strings.contains(block.data['tagFilter'], 'ALL'))
) or (
(p.category != null and block.data['categoryFilter'] != null and !#strings.isEmpty(block.data['categoryFilter']) and #strings.containsIgnoreCase(block.data['categoryFilter'], p.category.name))
or
(p.tags != null and block.data['tagFilter'] != null and !#strings.isEmpty(block.data['tagFilter']) and #strings.containsIgnoreCase(block.data['tagFilter'], p.tagNamesString))
)}">
<a th:href="@{'/posts/' + ${p.slug}}"
class="news-card"
th:data-category="${p.category != null ? p.category.name : ''}"
th:data-tags="${p.tags != null ? #strings.arrayJoin(p.tags.![name], ',') : ''}">
th:data-tags="${p.tags != null and !#sets.isEmpty(p.tags) ? #strings.listJoin(p.tags.![name], ',') : ''}">
<div class="card-thumb">
<img th:src="${p.featuredImage}" th:alt="${p.title}" />
<span class="year-badge" th:if="${p.createdDate != null}" th:text="${#temporals.format(p.createdDate, 'yyyy')}">2026</span>
@@ -224,28 +233,25 @@
if (!grid) return;
const cards = Array.from(grid.querySelectorAll('.news-card'));
const itemsPerPage = parseInt(grid.getAttribute('data-items-per-page')) || 9;
const itemsPerPage = Math.min(parseInt(grid.getAttribute('data-items-per-page')) || 12, 12);
let currentPage = 1;
let activeFilter = 'ALL';
// Render Tabs from posts.js multi-choice selection or extracted cards
if (tabsContainer) {
const rawConfigured = tabsContainer.getAttribute('data-configured-tabs') || '';
let tabNames = rawConfigured.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
// Fallback to extracting from cards if no specific multi-choice selection was made
if (tabNames.length === 0) {
const foundSet = new Set();
cards.forEach(function (card) {
const cat = card.getAttribute('data-category');
if (cat) foundSet.add(cat.trim());
if (cat && cat.trim()) foundSet.add(cat.trim());
const tags = (card.getAttribute('data-tags') || '').split(',');
tags.forEach(function (t) { if (t.trim()) foundSet.add(t.trim()); });
});
tabNames = Array.from(foundSet);
}
// Build tab button elements
tabNames.forEach(function (name) {
const btn = document.createElement('button');
btn.type = 'button';
@@ -255,7 +261,6 @@
tabsContainer.appendChild(btn);
});
// Single active tab switching
tabsContainer.addEventListener('click', function (e) {
const btn = e.target.closest('.tab-btn');
if (!btn) return;
@@ -274,29 +279,40 @@
});
}
// Render card visibility based on active tab & calculate pagination
function render() {
let visibleCards = cards.filter(function (card) {
if (activeFilter === 'ALL') return true;
const cardCat = (card.getAttribute('data-category') || '').trim();
const cardTags = (card.getAttribute('data-tags') || '').split(',').map(function (s) { return s.trim(); });
return cardCat === activeFilter || cardTags.includes(activeFilter);
const targetFilter = activeFilter.trim().toLowerCase();
const cardCat = (card.getAttribute('data-category') || '').trim().toLowerCase();
const cardTags = (card.getAttribute('data-tags') || '').split(',').map(function (s) { return s.trim().toLowerCase(); });
return cardCat === targetFilter || cardTags.includes(targetFilter);
});
const totalPages = Math.ceil(visibleCards.length / itemsPerPage) || 1;
if (currentPage > totalPages) currentPage = totalPages;
// Hide all
cards.forEach(function (c) { c.style.display = 'none'; });
// Show current page slice
let noPostsMsg = grid.querySelector('.no-posts-found');
if (visibleCards.length === 0) {
if (!noPostsMsg) {
noPostsMsg = document.createElement('div');
noPostsMsg.className = 'no-posts-found';
noPostsMsg.style.cssText = 'grid-column: 1 / -1; text-align: center; color: #64748b; padding: 40px 20px; font-size: 15px; background: #f8fafc; border-radius: 8px; border: 1px dashed #cbd5e1; margin: 20px 0;';
noPostsMsg.textContent = 'Chưa có bài viết nào thuộc danh mục/thẻ này.';
grid.appendChild(noPostsMsg);
}
noPostsMsg.style.display = 'block';
} else {
if (noPostsMsg) noPostsMsg.style.display = 'none';
}
const startIdx = (currentPage - 1) * itemsPerPage;
const endIdx = startIdx + itemsPerPage;
visibleCards.slice(startIdx, endIdx).forEach(function (c) {
c.style.display = 'flex';
});
// Render Pagination
if (pagination) {
pagination.innerHTML = '';
if (totalPages <= 1) return;