fix(blog): repair pagination on public and admin interfaces
CI/CD / CI · dotnet build (push) Successful in 35s
CI/CD / Deploy · drsousan (push) Successful in 28s

Public /blog: the handler param was named `page`, which is a reserved
route token in Razor Pages and never binds — so every page silently
showed the same first 10 posts. Renamed the query param to `pg`
([FromQuery(Name="pg")]) and updated the pagination links to match.

Admin: the posts table had no pagination and dumped all rows at once.
Added client-side pagination (10/page) with a prev/next + numbered bar
over the already-loaded posts array.

Verified: public page1=10/page2=4 with zero overlap; admin shows
‹ 1 2 › with correct row counts and active state per page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-11 00:23:22 +03:30
parent 427de7c0cb
commit 872e5c1818
3 changed files with 50 additions and 6 deletions
+3 -3
View File
@@ -101,16 +101,16 @@
<div class="pagination"> <div class="pagination">
@if (Model.CurrentPage > 1) @if (Model.CurrentPage > 1)
{ {
<a class="page-btn" href="/blog?page=@(Model.CurrentPage - 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")"></a> <a class="page-btn" href="/blog?pg=@(Model.CurrentPage - 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")"></a>
} }
@for (int p = 1; p <= Model.TotalPages; p++) @for (int p = 1; p <= Model.TotalPages; p++)
{ {
<a class="page-btn @(p == Model.CurrentPage ? "active" : "")" <a class="page-btn @(p == Model.CurrentPage ? "active" : "")"
href="/blog?page=@p@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")">@p</a> href="/blog?pg=@p@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")">@p</a>
} }
@if (Model.CurrentPage < Model.TotalPages) @if (Model.CurrentPage < Model.TotalPages)
{ {
<a class="page-btn" href="/blog?page=@(Model.CurrentPage + 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")"></a> <a class="page-btn" href="/blog?pg=@(Model.CurrentPage + 1)@(!string.IsNullOrEmpty(Model.ActiveCat) ? "&category=" + Model.ActiveCat : "")"></a>
} }
</div> </div>
} }
+4 -2
View File
@@ -20,9 +20,11 @@ public class BlogIndexModel : PageModel
public int TotalPosts { get; private set; } = 0; public int TotalPosts { get; private set; } = 0;
public string? ActiveCat { get; private set; } public string? ActiveCat { get; private set; }
public async Task<IActionResult> OnGetAsync(int page = 1, string? category = null) // NOTE: the query param is "pg", not "page" — "page" is a reserved route token in
// Razor Pages and never binds here, which silently pins every request to page 1.
public async Task<IActionResult> OnGetAsync([FromQuery(Name = "pg")] int pg = 1, string? category = null)
{ {
CurrentPage = page < 1 ? 1 : page; CurrentPage = pg < 1 ? 1 : pg;
ActiveCat = category; ActiveCat = category;
var q = _db.BlogPosts.Include(p => p.Category).Where(p => p.IsPublished); var q = _db.BlogPosts.Include(p => p.Category).Where(p => p.IsPublished);
+43 -1
View File
@@ -189,6 +189,13 @@ tr:hover td{background:#FAFBFC}
.page{display:none} .page{display:none}
.page.active{display:block} .page.active{display:block}
/* ── Table pagination ── */
.tbl-pagination{display:flex;gap:.4rem;justify-content:center;flex-wrap:wrap;padding:1rem 0 .25rem;margin-top:.5rem}
.tbl-page-btn{min-width:34px;height:34px;padding:0 .6rem;border-radius:8px;border:1.5px solid var(--border);background:#fff;font-family:'Vazirmatn',sans-serif;font-size:.85rem;color:var(--dark);cursor:pointer;transition:all .2s}
.tbl-page-btn:hover:not(:disabled){border-color:var(--gold);color:var(--gold)}
.tbl-page-btn.active{background:var(--gold);border-color:var(--gold);color:#fff}
.tbl-page-btn:disabled{opacity:.4;cursor:default}
/* ── Login ── */ /* ── Login ── */
.login-screen{position:fixed;inset:0;background:var(--dark);display:flex;align-items:center;justify-content:center;z-index:1000} .login-screen{position:fixed;inset:0;background:var(--dark);display:flex;align-items:center;justify-content:center;z-index:1000}
.login-box{background:var(--white);border-radius:20px;padding:2.5rem;width:380px;text-align:center} .login-box{background:var(--white);border-radius:20px;padding:2.5rem;width:380px;text-align:center}
@@ -496,6 +503,7 @@ tr:hover td{background:#FAFBFC}
<table><thead><tr><th>عنوان</th><th>دسته</th><th>کلیدواژه</th><th>بازدید</th><th>وضعیت</th><th>عملیات</th></tr></thead> <table><thead><tr><th>عنوان</th><th>دسته</th><th>کلیدواژه</th><th>بازدید</th><th>وضعیت</th><th>عملیات</th></tr></thead>
<tbody id="postsTable"></tbody></table> <tbody id="postsTable"></tbody></table>
</div> </div>
<div id="postsPagination" class="tbl-pagination"></div>
</div> </div>
</div> </div>
@@ -2026,9 +2034,23 @@ async function deleteCat(id){if(!confirm('حذف؟'))return;await api(`/api/blog
// ── Blog Posts ──────────────────────────────────────────────────────────────── // ── Blog Posts ────────────────────────────────────────────────────────────────
let posts=[]; let posts=[];
const POSTS_PER_PAGE = 10;
let postsPage = 1;
async function loadPosts(){ async function loadPosts(){
posts=await api('/api/blog/posts/admin')||[]; posts=await api('/api/blog/posts/admin')||[];
document.getElementById('postsTable').innerHTML=posts.map(p=>` postsPage = 1;
renderPostsPage();
}
function renderPostsPage(){
const totalPages = Math.max(1, Math.ceil(posts.length / POSTS_PER_PAGE));
if (postsPage > totalPages) postsPage = totalPages;
if (postsPage < 1) postsPage = 1;
const start = (postsPage - 1) * POSTS_PER_PAGE;
const slice = posts.slice(start, start + POSTS_PER_PAGE);
document.getElementById('postsTable').innerHTML = slice.map(p=>`
<tr> <tr>
<td><strong>${p.title}</strong></td> <td><strong>${p.title}</strong></td>
<td><span class="badge badge-gold">${p.category?.name||'—'}</span></td> <td><span class="badge badge-gold">${p.category?.name||'—'}</span></td>
@@ -2041,6 +2063,26 @@ async function loadPosts(){
<button class="btn btn-danger btn-sm" onclick="deletePost(${p.id})">حذف</button> <button class="btn btn-danger btn-sm" onclick="deletePost(${p.id})">حذف</button>
</td> </td>
</tr>`).join(''); </tr>`).join('');
renderPostsPagination(totalPages);
}
function renderPostsPagination(totalPages){
const box = document.getElementById('postsPagination');
if (!box) return;
if (totalPages <= 1) { box.innerHTML=''; return; }
let html = '';
html += `<button class="tbl-page-btn" ${postsPage===1?'disabled':''} onclick="gotoPostsPage(${postsPage-1})"></button>`;
for (let p=1; p<=totalPages; p++){
html += `<button class="tbl-page-btn ${p===postsPage?'active':''}" onclick="gotoPostsPage(${p})">${p}</button>`;
}
html += `<button class="tbl-page-btn" ${postsPage===totalPages?'disabled':''} onclick="gotoPostsPage(${postsPage+1})"></button>`;
box.innerHTML = html;
}
function gotoPostsPage(p){
postsPage = p;
renderPostsPage();
} }
async function openPostEditor(){ async function openPostEditor(){