mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(books): add jump rail navigation to book listings (#318)
Add server-backed jump buckets so large library, collection, and smart-scope listings can jump directly to sorted sections without loading every row up front. Closes #163
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { jumpBucketKindForSort } from "../jump-buckets";
|
||||
import type { SortSpec } from "../query";
|
||||
|
||||
describe("jumpBucketKindForSort", () => {
|
||||
it("returns letter for title sorts in both directions", () => {
|
||||
expect(jumpBucketKindForSort([{ field: "title", dir: "asc" }])).toBe("letter");
|
||||
expect(jumpBucketKindForSort([{ field: "title", dir: "desc" }])).toBe("letter");
|
||||
});
|
||||
|
||||
it("returns letter for author sorts in both directions", () => {
|
||||
expect(jumpBucketKindForSort([{ field: "author", dir: "asc" }])).toBe("letter");
|
||||
expect(jumpBucketKindForSort([{ field: "author", dir: "desc" }])).toBe("letter");
|
||||
});
|
||||
|
||||
it("returns year for publishedYear sorts", () => {
|
||||
expect(jumpBucketKindForSort([{ field: "publishedYear", dir: "asc" }])).toBe("year");
|
||||
expect(jumpBucketKindForSort([{ field: "publishedYear", dir: "desc" }])).toBe("year");
|
||||
});
|
||||
|
||||
it("returns letter for an empty sort (defaults to title asc)", () => {
|
||||
expect(jumpBucketKindForSort([])).toBe("letter");
|
||||
});
|
||||
|
||||
it("returns null for ineligible primary sort fields", () => {
|
||||
const ineligible: SortSpec[][] = [
|
||||
[{ field: "addedAt", dir: "desc" }],
|
||||
[{ field: "rating", dir: "desc" }],
|
||||
[{ field: "fileSize", dir: "asc" }],
|
||||
[{ field: "random", dir: "asc" }],
|
||||
];
|
||||
for (const sort of ineligible) {
|
||||
expect(jumpBucketKindForSort(sort)).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("only considers the primary sort field", () => {
|
||||
expect(
|
||||
jumpBucketKindForSort([
|
||||
{ field: "addedAt", dir: "desc" },
|
||||
{ field: "title", dir: "asc" },
|
||||
]),
|
||||
).toBeNull();
|
||||
expect(
|
||||
jumpBucketKindForSort([
|
||||
{ field: "title", dir: "asc" },
|
||||
{ field: "addedAt", dir: "desc" },
|
||||
]),
|
||||
).toBe("letter");
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ export * from "./library";
|
||||
export * from "./metadata-fetch";
|
||||
export * from "./pattern-resolver";
|
||||
export * from "./query";
|
||||
export * from "./jump-buckets";
|
||||
export * from "./reader-settings";
|
||||
export * from "./scanner";
|
||||
export * from "./opds";
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { SortField, SortSpec } from "./query";
|
||||
|
||||
export type JumpBucketKind = "letter" | "year";
|
||||
|
||||
/**
|
||||
* One jump target on the rail. `index` is the absolute 0-based row index of the
|
||||
* first book in this bucket under the listing's exact sort order, so a jump is
|
||||
* just scroll-to-index. Buckets are returned in display order (already reversed
|
||||
* for descending sorts).
|
||||
*/
|
||||
export type JumpBucket = {
|
||||
key: string;
|
||||
label: string;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export type JumpBucketsResponse = {
|
||||
buckets: JumpBucket[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
const KIND_BY_PRIMARY_SORT_FIELD: Partial<Record<SortField, JumpBucketKind>> = {
|
||||
title: "letter",
|
||||
author: "letter",
|
||||
publishedYear: "year",
|
||||
};
|
||||
|
||||
/**
|
||||
* Single source of truth for rail eligibility, shared by the client (gate the
|
||||
* rail and bucket fetches) and the server (validate + pick bucket expression).
|
||||
* Only the primary sort field matters: secondary sorts reorder rows within
|
||||
* equal primary values and cannot move bucket boundaries. An empty sort means
|
||||
* title ascending, mirroring the server's default.
|
||||
*/
|
||||
export function jumpBucketKindForSort(sort: SortSpec[]): JumpBucketKind | null {
|
||||
const primary = sort[0] ?? { field: "title", dir: "asc" };
|
||||
return KIND_BY_PRIMARY_SORT_FIELD[primary.field] ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user