From 8179670fdba44a9b1616482fbd3e03486a842a53 Mon Sep 17 00:00:00 2001 From: puppe1990 Date: Fri, 11 Sep 2026 12:47:58 -0300 Subject: [PATCH] Paginate the catalog and default the list to public repos Show twenty repositories per page with previous/next controls. Hide private repositories unless Visibility is Private or All. --- web/src/components/CatalogPager.svelte | 17 ++++++++ web/src/lib/catalogPage.js | 15 +++++++ web/src/lib/catalogPage.test.js | 26 ++++++++++++ web/src/lib/filter.js | 3 ++ web/src/lib/filter.test.js | 15 +++++++ web/src/pages/Home.svelte | 50 +++++++++++++++++------ web/src/pages/Home.test.js | 56 ++++++++++++++++++++++++++ web/static/css/atlas.css | 44 +++++++++++++++++++- 8 files changed, 212 insertions(+), 14 deletions(-) create mode 100644 web/src/components/CatalogPager.svelte create mode 100644 web/src/lib/catalogPage.js create mode 100644 web/src/lib/catalogPage.test.js diff --git a/web/src/components/CatalogPager.svelte b/web/src/components/CatalogPager.svelte new file mode 100644 index 0000000..b187e0c --- /dev/null +++ b/web/src/components/CatalogPager.svelte @@ -0,0 +1,17 @@ + + +{#if pages > 1} + +{/if} diff --git a/web/src/lib/catalogPage.js b/web/src/lib/catalogPage.js new file mode 100644 index 0000000..3e06ec5 --- /dev/null +++ b/web/src/lib/catalogPage.js @@ -0,0 +1,15 @@ +export const CATALOG_PAGE_SIZE = 20; + +export function sliceCatalogPage(items, page, size = CATALOG_PAGE_SIZE) { + const list = items || []; + const total = list.length; + const pages = Math.max(1, Math.ceil(total / size) || 1); + const current = Math.min(Math.max(1, Number(page) || 1), pages); + const start = (current - 1) * size; + return { + items: list.slice(start, start + size), + page: current, + pages, + total, + }; +} diff --git a/web/src/lib/catalogPage.test.js b/web/src/lib/catalogPage.test.js new file mode 100644 index 0000000..df1792c --- /dev/null +++ b/web/src/lib/catalogPage.test.js @@ -0,0 +1,26 @@ +import { describe, expect, test } from "vitest"; +import { CATALOG_PAGE_SIZE, sliceCatalogPage } from "./catalogPage.js"; + +const items = Array.from({ length: 45 }, (_, i) => ({ name: `repo-${i + 1}` })); + +describe("sliceCatalogPage", () => { + test("keeps twenty repositories on the first page", () => { + expect(CATALOG_PAGE_SIZE).toBe(20); + const page = sliceCatalogPage(items, 1); + expect(page.items.map((repo) => repo.name)).toEqual(items.slice(0, 20).map((repo) => repo.name)); + expect(page.page).toBe(1); + expect(page.pages).toBe(3); + expect(page.total).toBe(45); + }); + + test("returns the remainder on the last page", () => { + const page = sliceCatalogPage(items, 3); + expect(page.items.map((repo) => repo.name)).toEqual(["repo-41", "repo-42", "repo-43", "repo-44", "repo-45"]); + expect(page.page).toBe(3); + }); + + test("clamps a page past the end back onto the last page", () => { + expect(sliceCatalogPage(items, 99).page).toBe(3); + expect(sliceCatalogPage(items, 0).page).toBe(1); + }); +}); diff --git a/web/src/lib/filter.js b/web/src/lib/filter.js index 449a0c2..8001d6d 100644 --- a/web/src/lib/filter.js +++ b/web/src/lib/filter.js @@ -54,6 +54,7 @@ export function applyFilters(repos, filters) { const query = (filters.query || "").toLowerCase(); const includeForks = !!filters.includeForks; const hasHomepage = !!filters.hasHomepage; + const visibility = filters.visibility || "public"; const sortBy = filters.sortBy || "stars"; const sortOrder = filters.sortOrder || "desc"; const sortThen = (filters.sortThen || "").trim(); @@ -66,6 +67,8 @@ export function applyFilters(repos, filters) { const text = `${repo.name || ""} ${repo.description || ""}`.toLowerCase(); if (query && !text.includes(query)) return false; if (!includeForks && repo.fork) return false; + if (visibility === "public" && repo.private) return false; + if (visibility === "private" && !repo.private) return false; const homepage = (repo.homepage || "").trim(); if (hasHomepage && !homepage) return false; return true; diff --git a/web/src/lib/filter.test.js b/web/src/lib/filter.test.js index c047bf1..d3cff52 100644 --- a/web/src/lib/filter.test.js +++ b/web/src/lib/filter.test.js @@ -23,6 +23,21 @@ describe("applyFilters", () => { expect(got).toHaveLength(0); }); + test("lists public repositories by default and can show private or all", () => { + const mixed = [ + { name: "open", private: false, stargazers_count: 1 }, + { name: "secret", private: true, stargazers_count: 1 }, + ]; + expect(applyFilters(mixed, { includeForks: true, sortBy: "name", sortOrder: "asc" }).map((r) => r.name)).toEqual(["open"]); + expect(applyFilters(mixed, { includeForks: true, visibility: "private", sortBy: "name", sortOrder: "asc" }).map((r) => r.name)).toEqual([ + "secret", + ]); + expect(applyFilters(mixed, { includeForks: true, visibility: "all", sortBy: "name", sortOrder: "asc" }).map((r) => r.name)).toEqual([ + "open", + "secret", + ]); + }); + test("sorts by traffic views and clones", () => { const withTraffic = [ { name: "quiet", traffic: { views: 2, clones: 80, view_uniques: 2, clone_uniques: 10 } }, diff --git a/web/src/pages/Home.svelte b/web/src/pages/Home.svelte index 3e423e0..a432b5e 100644 --- a/web/src/pages/Home.svelte +++ b/web/src/pages/Home.svelte @@ -3,8 +3,10 @@ import ThemeToggle from "../components/ThemeToggle.svelte"; import RepoCard from "../components/RepoCard.svelte"; import CatalogCharts from "../components/CatalogCharts.svelte"; + import CatalogPager from "../components/CatalogPager.svelte"; import SortModal from "../components/SortModal.svelte"; import { applyFilters, languageOptions } from "../lib/filter.js"; + import { sliceCatalogPage } from "../lib/catalogPage.js"; import { absoluteUrl, formatCount } from "../lib/format.js"; export let profile = {}; @@ -21,6 +23,7 @@ let query = ""; let includeForks = true; let hasHomepage = false; + let visibility = "public"; let sortBy = "stars"; let sortOrder = "desc"; let sortThen = ""; @@ -29,13 +32,21 @@ let loading = false; let preferTraffic = false; let view = "catalog"; + let page = 1; + let lastFilterKey = ""; $: if (!preferTraffic && (repos || []).some((repo) => repo.traffic?.available)) { sortBy = "traffic"; preferTraffic = true; } $: langs = languageOptions(repos); - $: visible = applyFilters(repos, { language, minStars, query, includeForks, hasHomepage, sortBy, sortOrder, sortThen, sortThenOrder }); + $: visible = applyFilters(repos, { language, minStars, query, includeForks, hasHomepage, visibility, sortBy, sortOrder, sortThen, sortThenOrder }); + $: filterKey = [language, minStars, query, includeForks, hasHomepage, visibility, sortBy, sortOrder, sortThen, sortThenOrder].join("|"); + $: if (filterKey !== lastFilterKey) { + lastFilterKey = filterKey; + page = 1; + } + $: catalogPage = sliceCatalogPage(visible, page); $: if (typeof document !== "undefined") { document.body.classList.toggle("is-populated", !!populated); } @@ -195,6 +206,14 @@ {/each} +