From 417f6ecb240102f8a21a3b7a796a084e26d47c24 Mon Sep 17 00:00:00 2001 From: Victor Phan Date: Mon, 3 Aug 2026 20:34:15 +0700 Subject: [PATCH] refactor: update card grid filtering and pagination logic to support category/tag filters and display empty states --- .../content_main_blocks/card_grid_layout.html | 50 ++++++++++++------- .../web/config/SecurityConfiguration.java | 2 +- .../com/sisvietnamvn/web/domain/Post.java | 16 ++++++ .../static/js/manage/editor-plugins/posts.js | 12 +++-- .../src/main/resources/templates/page.html | 40 ++++++++++++--- 5 files changed, 90 insertions(+), 30 deletions(-) diff --git a/sisvietnamvn_main/html_snippets/content_main_blocks/card_grid_layout.html b/sisvietnamvn_main/html_snippets/content_main_blocks/card_grid_layout.html index c718d71d..43c02e11 100644 --- a/sisvietnamvn_main/html_snippets/content_main_blocks/card_grid_layout.html +++ b/sisvietnamvn_main/html_snippets/content_main_blocks/card_grid_layout.html @@ -188,13 +188,22 @@ - -
- + +
+ + th:data-tags="${p.tags != null and !#sets.isEmpty(p.tags) ? #strings.listJoin(p.tags.![name], ',') : ''}">
2026 @@ -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; diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/config/SecurityConfiguration.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/config/SecurityConfiguration.java index a338ab6e..bc5bf950 100644 --- a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/config/SecurityConfiguration.java +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/config/SecurityConfiguration.java @@ -62,7 +62,7 @@ public class SecurityConfiguration { authz .requestMatchers(HttpMethod.GET, "/", "/about", "/flex-finish", "/tin-tuc", "/tin-tuc/**", "/lien-he", "/manage/login", "/css/**", "/images/**", "/js/**", "/vendor/**", "/fonts/**", "/login-assets/**", "/UMass*/**", "/Undergraduate*/**", - "/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/upload/**", "/api/manage/snippets/**", "/api/categories", "/api/tags", "/page/**", "/pages/**", "/news/article/**", "/post/**", "/error", + "/favicon.ico", "/favicons/**", "/flex-finish/**", "/contact-us/**", "/uploads/**", "/upload/**", "/api/manage/snippets/**", "/api/categories", "/api/tags", "/page/**", "/pages/**", "/news/article/**", "/post/**", "/posts", "/posts/**", "/error", "/about-us", "/specialty", "/doctor", "/bac-si/**", "/service", "/health-library", "/news-and-events", "/patient-support", "/medical-expert", "/umcers", "/bidding", "/contact-us", "/theme-assets/**", "/dao-tao/**", "/chuyen-khoa", "/chuyen-khoa/**", "/Đào tạo tại UMC_files/**", "/dat-lich", "/dat-lich/**", "/api/public/doctor-schedule/**") .permitAll() diff --git a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Post.java b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Post.java index 06e5bd33..daa0cfbb 100644 --- a/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Post.java +++ b/sisvietnamvn_main/src/main/java/com/sisvietnamvn/web/domain/Post.java @@ -2,7 +2,9 @@ package com.sisvietnamvn.web.domain; import java.io.Serial; import java.util.HashSet; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -101,6 +103,20 @@ public class Post extends AbstractAuditingEntity { ) private Set tags = new HashSet<>(); + public String getCategoryName() { + return category != null ? category.getName() : ""; + } + + public String getTagNamesString() { + if (tags == null || tags.isEmpty()) { + return ""; + } + return tags.stream() + .map(Tag::getName) + .filter(Objects::nonNull) + .collect(Collectors.joining(",")); + } + // --- Getters and Setters --- @Override diff --git a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/posts.js b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/posts.js index 0f71dad5..2874d4da 100644 --- a/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/posts.js +++ b/sisvietnamvn_main/src/main/resources/static/js/manage/editor-plugins/posts.js @@ -29,7 +29,7 @@ class SISPostsTool { statusFilter: data && data.statusFilter ? data.statusFilter : 'PUBLISHED', displayStyle: data && data.displayStyle ? data.displayStyle : 'grid', // 'grid', 'list', 'cards', 'compact' cols: parseInt(data && data.cols) || 3, - limit: parseInt(data && data.limit) || 6, + limit: parseInt(data && data.limit) || 12, orderBy: data && data.orderBy ? data.orderBy : 'createdDate_desc', locationFilter: data && data.locationFilter ? data.locationFilter : '', postIds: data && data.postIds ? data.postIds : '', // Optional manually comma-separated IDs @@ -37,7 +37,7 @@ class SISPostsTool { showExcerpt: data && data.showExcerpt !== undefined ? !!data.showExcerpt : true, showDate: data && data.showDate !== undefined ? !!data.showDate : true, showLocation: data && data.showLocation !== undefined ? !!data.showLocation : true, - itemsPerPage: parseInt(data && data.itemsPerPage) || parseInt(data && data.limit) || 9, + itemsPerPage: parseInt(data && data.itemsPerPage) || parseInt(data && data.limit) || 12, enablePagination: data && data.enablePagination !== undefined ? !!data.enablePagination : true, enableTabs: data && data.enableTabs !== undefined ? !!data.enableTabs : true, globalId: data && data.globalId ? data.globalId : '', @@ -114,7 +114,7 @@ class SISPostsTool { // 3. Limit (Number of posts) const limitDiv = document.createElement('div'); limitDiv.className = 'col-md-2 mb-2'; - limitDiv.innerHTML = ''; + limitDiv.innerHTML = ''; const limitInput = document.createElement('input'); limitInput.type = 'number'; limitInput.className = 'form-control form-control-sm'; @@ -122,7 +122,11 @@ class SISPostsTool { limitInput.min = 1; limitInput.max = 50; if (this.readOnly) limitInput.disabled = true; - limitInput.addEventListener('input', (e) => this.data.limit = parseInt(e.target.value) || 6); + limitInput.addEventListener('input', (e) => { + const val = parseInt(e.target.value) || 12; + this.data.limit = val; + this.data.itemsPerPage = val; + }); limitDiv.appendChild(limitInput); formRow1.appendChild(limitDiv); diff --git a/sisvietnamvn_main/src/main/resources/templates/page.html b/sisvietnamvn_main/src/main/resources/templates/page.html index ea242ae4..09f47f60 100644 --- a/sisvietnamvn_main/src/main/resources/templates/page.html +++ b/sisvietnamvn_main/src/main/resources/templates/page.html @@ -621,12 +621,21 @@
-
- +
+ + th:data-tags="${p.tags != null and !#sets.isEmpty(p.tags) ? #strings.listJoin(p.tags.![name], ',') : ''}">
2026 @@ -656,7 +665,7 @@ 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'; @@ -668,7 +677,7 @@ 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()); }); }); @@ -705,9 +714,10 @@ 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; @@ -715,6 +725,20 @@ cards.forEach(function (c) { c.style.display = 'none'; }); + 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) {