Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions spike/shots.mjs

This file was deleted.

31 changes: 30 additions & 1 deletion src/lib/daily.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,40 @@ export async function validateToken(token: string): Promise<boolean> {
}

// Response shape per OpenAPI: { data: BookmarkedPost[], pagination: { cursor, hasNextPage } }
// `total` is read defensively: the current API does not document it, but if a
// future version returns it we can report an exact count from a single page.
type BookmarksPage = {
data: Bookmark[];
pagination?: { cursor?: string | null; hasNextPage?: boolean };
pagination?: { cursor?: string | null; hasNextPage?: boolean; total?: number };
total?: number;
};

/**
* Cheap chamber tally for the homepage. Fetches a single page instead of
* paginating the whole pile (which `listAllBookmarks` would do just to count).
* Returns an exact count when the API exposes a total or the pile fits in one
* page; otherwise `exact` is false and the count is the first-page size, which
* the UI renders as "N+".
*/
export async function countBookmarks(
token: string,
opts: { unreadOnly?: boolean } = {},
): Promise<{ count: number; exact: boolean }> {
const url = new URL(`${BASE}/bookmarks/`);
url.searchParams.set("limit", "50");
if (opts.unreadOnly) url.searchParams.set("unreadOnly", "true");

const res = await fetch(url, { headers: authHeaders(token) });
if (!res.ok) throw new Error(`countBookmarks failed: ${res.status}`);

const body = (await res.json()) as BookmarksPage;
const total = body.pagination?.total ?? body.total;
if (typeof total === "number") return { count: total, exact: true };

const items = body.data ?? [];
return { count: items.length, exact: !(body.pagination?.hasNextPage ?? false) };
}

/** Fetches one page (max 50). `unreadOnly` targets the dead-weight pile we want to cull. */
export async function listBookmarks(
token: string,
Expand Down
22 changes: 22 additions & 0 deletions src/pages/api/bookmarks/count.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { APIRoute } from "astro";
import { countBookmarks } from "../../../lib/daily";
import { getToken } from "../../../lib/session";

// GET /api/bookmarks/count?unreadOnly=true — a lightweight tally for the
// homepage "in the chamber" counter. Unlike /api/bookmarks, this hits daily.dev
// once (a single page) instead of paginating the whole pile just to count.
export const GET: APIRoute = async ({ url, cookies }) => {
const token = getToken(cookies);
if (!token) return new Response(JSON.stringify({ error: "Not signed in" }), { status: 401 });

const unreadOnly = url.searchParams.get("unreadOnly") === "true";
try {
const { count, exact } = await countBookmarks(token, { unreadOnly });
return new Response(JSON.stringify({ count, exact }), {
headers: { "Content-Type": "application/json" },
});
} catch (err) {
console.error("[bookmarks:count] ", err);
return new Response(JSON.stringify({ error: "Failed to count bookmarks" }), { status: 502 });
}
};
8 changes: 6 additions & 2 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,13 @@ const profile = await currentUser(Astro.cookies);

const countEl = document.getElementById("bm-count");
if (countEl) {
fetch("/api/bookmarks")
// Lightweight count endpoint: one daily.dev call, not a full paginated load.
fetch("/api/bookmarks/count")
.then((r) => r.json())
.then((d) => { countEl.textContent = String(d.items?.length ?? 0); })
.then((d) => {
const n = d.count ?? 0;
countEl.textContent = d.exact ? String(n) : `${n}+`;
})
.catch(() => { countEl.textContent = "?"; });
}
</script>
Expand Down
Loading
Loading