sync
CI / backend-and-migrations (push) Canceled after 0s
CI / astro-build (push) Canceled after 0s
CI / dependency-audit (push) Canceled after 0s
CI / compose-e2e (push) Canceled after 0s

This commit is contained in:
ik
2026-09-17 07:35:23 +07:00
parent 722c88d436
commit d438c40542
21 changed files with 2181 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
---
import { pageHref, pageWindow } from "../lib/pagination";
interface Props {
path: string;
params: URLSearchParams;
total: number;
limit: number;
offset: number;
anchor?: string;
itemLabel?: string;
}
const { path, params, total, limit, offset, anchor = "", itemLabel = "записей" } = Astro.props;
const state = pageWindow(total, limit, offset);
---
{total > limit && <nav class="pagination" aria-label="Пагинация">
<span>{state.start}{state.end} из {total} {itemLabel} · страница {state.page} из {state.pages}</span>
<div>
{state.previousOffset !== null
? <a data-action="secondary" rel="prev" href={pageHref(path, params, state.previousOffset, anchor)}>← Предыдущая</a>
: <span class="pagination__disabled" aria-disabled="true">← Предыдущая</span>}
{state.nextOffset !== null
? <a data-action="secondary" rel="next" href={pageHref(path, params, state.nextOffset, anchor)}>Следующая →</a>
: <span class="pagination__disabled" aria-disabled="true">Следующая →</span>}
</div>
</nav>}
+31
View File
@@ -0,0 +1,31 @@
export type PageWindow = {
start: number;
end: number;
page: number;
pages: number;
previousOffset: number | null;
nextOffset: number | null;
};
export const pageWindow = (total: number, limit: number, offset: number): PageWindow => {
const safeTotal = Math.max(0, Math.trunc(total));
const safeLimit = Math.max(1, Math.trunc(limit));
const safeOffset = Math.max(0, Math.trunc(offset));
const end = Math.min(safeOffset + safeLimit, safeTotal);
return {
start: safeTotal > 0 && safeOffset < safeTotal ? safeOffset + 1 : 0,
end,
page: Math.floor(safeOffset / safeLimit) + 1,
pages: Math.max(1, Math.ceil(safeTotal / safeLimit)),
previousOffset: safeOffset > 0 ? Math.max(0, safeOffset - safeLimit) : null,
nextOffset: end < safeTotal ? safeOffset + safeLimit : null,
};
};
export const pageHref = (path: string, search: URLSearchParams, offset: number, anchor = "") => {
const params = new URLSearchParams(search);
if (offset > 0) params.set("offset", String(offset));
else params.delete("offset");
const query = params.toString();
return `${path}${query ? `?${query}` : ""}${anchor}`;
};
+1
View File
@@ -0,0 +1 @@
.pagination{width:min(1360px,calc(100% - 64px));margin:22px auto 48px;display:flex;align-items:center;justify-content:space-between;gap:16px;color:var(--text-secondary);font-size:13px}.pagination>div{display:flex;gap:10px}.pagination a,.pagination__disabled{display:inline-flex;align-items:center;min-height:42px;padding:10px 15px;border:1px solid var(--border-strong);border-radius:10px;background:var(--surface-elevated);color:var(--text-secondary);text-decoration:none}.pagination a:hover{background:var(--surface-raised);color:var(--text-primary)}.pagination__disabled{opacity:.5}@media(max-width:720px){.pagination{width:calc(100% - 28px);align-items:stretch;flex-direction:column}.pagination>div{display:grid;grid-template-columns:1fr 1fr}.pagination a,.pagination__disabled{justify-content:center;text-align:center}}