pagnation
This commit is contained in:
+127
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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); }
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user