feat: default-sort the website table by downloads per month

Entries with a download count now sort first (descending), with
stars, then Built-in, then name as fallback tiers for entries that
lack a count. main.js mirrors this in its default activeSort, clean
URL check, and third-click reset target. Sorting by stars remains one
header click away.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vinta Chen
2026-08-16 19:15:21 +08:00
co-authored by Claude
parent b841db68ba
commit d3fec1f7fa
3 changed files with 14 additions and 8 deletions
+9 -5
View File
@@ -115,20 +115,24 @@ def load_downloads(path: Path) -> dict[str, int]:
def sort_entries(entries: Sequence[TemplateEntry]) -> list[TemplateEntry]:
"""Sort entries by stars descending, then name ascending.
"""Sort entries by downloads descending, then name ascending.
Three tiers: starred entries first, stdlib second, other non-starred last.
Four tiers: entries with download counts first, then starred entries by
stars descending, then stdlib, then the rest.
"""
def sort_key(entry: TemplateEntry) -> tuple[int, int, int, str]:
downloads = entry["downloads"]
stars = entry["stars"]
name = entry["name"].lower()
if downloads is not None:
return (0, -downloads, 0, name)
if stars is not None:
builtin = 1 if entry.get("source_type") == "Built-in" else 0
return (0, -stars, builtin, name)
return (1, -stars, builtin, name)
if entry.get("source_type") == "Built-in":
return (1, 0, 0, name)
return (2, 0, 0, name)
return (2, 0, 0, name)
return (3, 0, 0, name)
return sorted(entries, key=sort_key)
+3 -3
View File
@@ -5,7 +5,7 @@ function getScrollBehavior() {
}
let activeFilter = null;
let activeSort = { col: "stars", order: "desc" };
let activeSort = { col: "downloads", order: "desc" };
const searchInput = document.querySelector(".search");
const filterBar = document.querySelector(".filter-bar");
const filterValue = document.querySelector(".filter-value");
@@ -211,7 +211,7 @@ function buildQueryString() {
const params = new URLSearchParams();
const query = searchInput ? searchInput.value.trim() : "";
if (query) params.set("q", query);
if (activeSort.col !== "stars" || activeSort.order !== "desc") {
if (activeSort.col !== "downloads" || activeSort.order !== "desc") {
params.set("sort", activeSort.col);
params.set("order", activeSort.order);
}
@@ -392,7 +392,7 @@ sortHeaders.forEach(function (th) {
if (activeSort.col === col) {
if (activeSort.order === defaultOrder)
activeSort = { col: col, order: altOrder };
else activeSort = { col: "stars", order: "desc" };
else activeSort = { col: "downloads", order: "desc" };
} else {
activeSort = { col: col, order: defaultOrder };
}
+2
View File
@@ -463,6 +463,8 @@ class TestBuild:
assert "1,234,567" in html
# Built-in entries never show PyPI counts: the asyncio row is the backport package
assert "26,305,454" not in html
# Default sort: entries with download counts come first
assert html.index("My-Lib") < html.index("no-pypi")
def test_build_fails_when_group_and_category_slug_collide(self, tmp_path):
readme = textwrap.dedent("""\