pagnation

This commit is contained in:
Mateusz Gruszczyński
2026-08-02 14:26:32 +02:00
parent 464660a274
commit f71ebefa82
7 changed files with 290 additions and 20 deletions
+127 -1
View File
@@ -6355,4 +6355,130 @@ dialog::backdrop {
padding-left: 0;
font-size: .68rem;
}
}
}
/* Search, pagination and theme-aware scrollbars. */
* {
scrollbar-width: thin;
scrollbar-color: var(--border-strong) var(--surface-inset);
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: var(--surface-inset);
}
*::-webkit-scrollbar-thumb {
border: 2px solid var(--surface-inset);
border-radius: 999px;
background: var(--border-strong);
}
*::-webkit-scrollbar-thumb:hover {
background: var(--muted-2);
}
.list-controls,
.notes-toolbar {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.resources-controls {
margin-top: 18px;
}
.list-search {
flex: 1 1 240px;
}
.list-search input {
width: 100%;
min-height: 40px;
}
.page-size-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: .82rem;
font-weight: 600;
white-space: nowrap;
}
.page-size-label select {
min-width: 78px;
min-height: 40px;
padding: 0 34px 0 12px;
border: 1px solid var(--border-strong);
border-radius: 10px;
background-color: var(--surface-toolbar);
background-image: linear-gradient(45deg, transparent 50%, var(--select-arrow) 50%),
linear-gradient(135deg, var(--select-arrow) 50%, transparent 50%);
background-position: calc(100% - 16px) 16px, calc(100% - 11px) 16px;
background-repeat: no-repeat;
background-size: 5px 5px, 5px 5px;
box-shadow: inset 0 1px 0 var(--wash-soft);
color: var(--text);
font-weight: 700;
appearance: none;
-webkit-appearance: none;
cursor: pointer;
transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
}
.page-size-label select:hover {
border-color: var(--select-border);
background-color: var(--surface-hover);
}
.page-size-label select:focus-visible {
border-color: var(--focus);
outline: none;
box-shadow: 0 0 0 3px var(--focus-ring), inset 0 1px 0 var(--wash-soft);
}
.page-size-label:focus-within {
color: var(--text-secondary);
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-top: 16px;
color: var(--muted);
font-size: .88rem;
}
.pagination button {
min-height: 36px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-2);
color: var(--text);
cursor: pointer;
}
.pagination button:disabled {
opacity: .45;
cursor: not-allowed;
}
@media (max-width: 620px) {
.pagination {
justify-content: space-between;
}
.pagination span {
text-align: center;
}
}
+5
View File
@@ -125,7 +125,12 @@
adds link-based protection. Private items are visible only to their owner and explicitly shared accounts or
valid share links. Unauthorized visitors receive a not-found response.</p>
</header>
<div class="list-controls resources-controls">
<label class="list-search"><span class="sr-only">Search notes and workspaces</span><input id="resources-search" type="search" placeholder="Search notes and workspaces…" autocomplete="off"></label>
<label class="page-size-label">Per page<select id="resources-per-page"><option value="25">25</option><option value="50">50</option><option value="100">100</option></select></label>
</div>
<div id="resources-list" class="resources-list"></div>
<nav id="resources-pagination" class="pagination" aria-label="Resources pagination"></nav>
<p id="resources-error" class="form-message error" role="alert"></p>
</div>
</dialog>
+18 -3
View File
@@ -107,6 +107,11 @@ const registrationEnabled = document.body.dataset.registrationEnabled === "true"
const resourcesDialog = document.querySelector("#resources-dialog");
const resourcesList = document.querySelector("#resources-list");
const resourcesError = document.querySelector("#resources-error");
const resourcesSearch = document.querySelector("#resources-search");
const resourcesPerPage = document.querySelector("#resources-per-page");
const resourcesPagination = document.querySelector("#resources-pagination");
let resourcesPage = 1;
let resourcesSearchTimer;
const profileDialog = document.querySelector("#profile-dialog");
const profileForm = document.querySelector("#profile-form");
@@ -116,11 +121,16 @@ function authHeaders() { return {}; }
function escapeHtml(value) { const node = document.createElement("div"); node.textContent = String(value ?? ""); return node.innerHTML; }
function shareExpiry(hours, forever) { if (forever) return null; const value = Number(hours); if (!Number.isFinite(value) || value <= 0 || value > 87600) throw new Error("Enter a validity between 1 and 87600 hours."); return new Date(Date.now() + value * 3600000).toISOString(); }
function formatShareExpiry(value) { if (!value) return "Never expires"; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : `Expires ${date.toLocaleString()}`; }
function renderResourcesPagination(meta) {
resourcesPagination.innerHTML = meta.total ? `<button type="button" data-page="${meta.page - 1}" ${meta.page <= 1 ? "disabled" : ""}>Previous</button><span>Page ${meta.page} of ${meta.total_pages} · ${meta.total} items</span><button type="button" data-page="${meta.page + 1}" ${meta.page >= meta.total_pages ? "disabled" : ""}>Next</button>` : "";
}
async function loadResources() {
resourcesError.textContent = ""; resourcesList.innerHTML = "<p>Loading…</p>";
try {
const data = await api("/api/auth/resources", { headers: authHeaders() });
const items = [...data.workspaces.map(item => ({ ...item, kind: "workspace", url: `/w/${item.slug}` })), ...data.pads.map(item => ({ ...item, kind: "pad", url: `/p/${item.slug}` }))];
const params = new URLSearchParams({ q: resourcesSearch.value.trim(), page: String(resourcesPage), per_page: resourcesPerPage.value });
const data = await api(`/api/auth/resources?${params}`, { headers: authHeaders() });
resourcesPage = data.pagination.page;
const items = data.items.map(item => ({ ...item, url: item.kind === "workspace" ? `/w/${item.slug}` : `/p/${item.slug}` }));
resourcesList.innerHTML = items.length ? "" : "<p>No assigned items yet.</p>";
for (const item of items) {
const row = document.createElement("article");
@@ -256,9 +266,14 @@ async function loadResources() {
});
resourcesList.append(row);
}
} catch (e) { resourcesList.innerHTML = ""; resourcesError.textContent = e.message; }
renderResourcesPagination(data.pagination);
} catch (e) { resourcesList.innerHTML = ""; resourcesPagination.innerHTML = ""; resourcesError.textContent = e.message; }
}
resourcesSearch?.addEventListener("input", () => { clearTimeout(resourcesSearchTimer); resourcesSearchTimer = setTimeout(() => { resourcesPage = 1; loadResources(); }, 250); });
resourcesPerPage?.addEventListener("change", () => { resourcesPage = 1; loadResources(); });
resourcesPagination?.addEventListener("click", event => { const button = event.target.closest("[data-page]"); if (!button || button.disabled) return; resourcesPage = Number(button.dataset.page) || 1; loadResources(); });
function renderAccount(session) {
currentSession = session;
+15 -1
View File
@@ -29,6 +29,11 @@ const dialog = document.querySelector("#password-dialog");
const identityDialog = document.querySelector("#identity-dialog");
const workspaceContent = document.querySelector("#workspace-content");
const notesList = document.querySelector("#notes-list");
const notesSearch = document.querySelector("#notes-search");
const notesPerPage = document.querySelector("#notes-per-page");
const notesPagination = document.querySelector("#notes-pagination");
let notesPage = 1;
let notesSearchTimer;
const notesViewKey = `rustpad:workspace:${slug}:notes-view`;
let notesView = localStorage.getItem(notesViewKey) === "table" ? "table" : "grid";
let notesCache = [];
@@ -115,15 +120,21 @@ async function showSystemNotFound() {
document.body.textContent = "404 Not Found";
}
}
function renderNotesPagination(meta) {
notesPagination.innerHTML = meta.total ? `<button type="button" data-page="${meta.page - 1}" ${meta.page <= 1 ? "disabled" : ""}>Previous</button><span>Page ${meta.page} of ${meta.total_pages} · ${meta.total} notes</span><button type="button" data-page="${meta.page + 1}" ${meta.page >= meta.total_pages ? "disabled" : ""}>Next</button>` : "";
}
async function openWorkspace() {
try {
const data = await api(`/api/workspaces/${encodeURIComponent(slug)}/open`, { method: "POST", body: JSON.stringify({ access_token: accessToken || null }) });
const params = new URLSearchParams({ q: notesSearch.value.trim(), page: String(notesPage), per_page: notesPerPage.value });
const data = await api(`/api/workspaces/${encodeURIComponent(slug)}/open?${params}`, { method: "POST", body: JSON.stringify({ access_token: accessToken || null }) });
info = data.workspace;
document.querySelector("#workspace-title").textContent = info.title;
document.querySelector("#workspace-url").textContent = location.pathname;
document.title = `${info.title} · RustPad`;
notesPage = data.pagination.page;
notesCache = data.notes;
renderNotes();
renderNotesPagination(data.pagination);
if (dialog.open) dialog.close();
} catch (e) {
if (info?.protected || e.message.toLowerCase().includes("password")) {
@@ -190,6 +201,9 @@ document.querySelectorAll("[data-notes-view]").forEach(button => button.addEvent
notesView = button.dataset.notesView;
renderNotes();
}));
notesSearch.addEventListener("input", () => { clearTimeout(notesSearchTimer); notesSearchTimer = setTimeout(() => { notesPage = 1; openWorkspace(); }, 250); });
notesPerPage.addEventListener("change", () => { notesPage = 1; openWorkspace(); });
notesPagination.addEventListener("click", event => { const button = event.target.closest("[data-page]"); if (!button || button.disabled) return; notesPage = Number(button.dataset.page) || 1; openWorkspace(); });
document.querySelector("#copy-workspace-link").addEventListener("click", async () => {
try { await copyText(new URL(location.pathname, location.origin).href); toast("Link copied"); }
catch (e) { toast(e.message); }
+5 -1
View File
@@ -34,13 +34,17 @@
<p>Select a note or create a new one.</p>
</div><button id="new-note-button" class="primary-button inline-button">New note</button>
</section>
<div class="notes-toolbar"><span class="notes-toolbar__label">View</span>
<div class="notes-toolbar">
<label class="list-search"><span class="sr-only">Search notes</span><input id="notes-search" type="search" placeholder="Search notes…" autocomplete="off"></label>
<label class="page-size-label">Per page<select id="notes-per-page"><option value="25">25</option><option value="50">50</option><option value="100">100</option></select></label>
<span class="notes-toolbar__label">View</span>
<div class="notes-view-switch" role="group" aria-label="Notes view"><button type="button"
data-notes-view="grid" class="active" aria-pressed="true">Cards</button><button type="button"
data-notes-view="table" aria-pressed="false">Table</button></div>
</div>
<p id="workspace-error" class="form-message error"></p>
<section id="notes-list" class="notes-grid" aria-live="polite"></section>
<nav id="notes-pagination" class="pagination" aria-label="Notes pagination"></nav>
</main>
<dialog id="identity-dialog">
<form id="identity-form" autocomplete="on" class="dialog-panel identity-panel">