feat: extract built-in view presets and add filter chips

This commit is contained in:
2026-08-11 20:08:53 -04:00
parent 4c3fd66a56
commit 0139f6f5eb
4 changed files with 131 additions and 32 deletions
@@ -0,0 +1,27 @@
<script lang="ts">
import { BOOK_PRESETS } from '$lib/presets';
import { getBookCollectionState } from '$lib/state/bookCollection.svelte';
const bookCollection = getBookCollectionState();
</script>
<!--
Built-in views, not shelves: a fixed set every library has, so nothing here is
named, owned or persisted. The same definitions drive the home page's shelves,
so "Recently added" means the same thing in both places.
-->
<div class="flex items-center gap-1.5 overflow-x-auto" aria-label="Preset views">
{#each BOOK_PRESETS as preset (preset.id)}
{@const active = bookCollection.isPresetActive(preset)}
<button
type="button"
aria-pressed={active}
onclick={() => (active ? bookCollection.clearView() : bookCollection.applyPreset(preset))}
class="shrink-0 rounded-full border px-3 py-1 text-xs whitespace-nowrap transition-colors {active
? 'border-primary bg-primary text-primary-foreground font-medium'
: 'border-border text-muted-foreground hover:border-muted-foreground hover:text-foreground'}"
>
{preset.label}
</button>
{/each}
</div>
+79
View File
@@ -0,0 +1,79 @@
/**
* Built-in views over a library.
*
* These are navigation, not content — a fixed set of lenses every library has,
* as distinct from shelves, which are named collections a user owns. Nothing
* here is persisted or user-editable.
*
* Defined once and consumed twice: the home route renders them as shelves, and
* the library view renders them as chips. Previously each home shelf was a
* hand-written query string in the loader, which is how `create_at` (missing a
* `d`) went unnoticed — that sort silently did nothing.
*/
export interface BookPreset {
id: string;
label: string;
/** Shown as the shelf heading on the home page. */
heading: string;
/** Filter values, keyed the way the books endpoint expects them. */
filters: Record<string, string[]>;
/** Backend sort field; omitted means "leave the current ordering alone". */
orderBy?: string;
/** asc, desc, or random — the API's CustomOrderBy accepts all three. */
sortOrder?: string;
}
export const BOOK_PRESETS: BookPreset[] = [
{
id: 'continue-reading',
label: 'Reading',
heading: 'Continue Reading',
filters: { progress: ['in_progress'] },
orderBy: 'last_accessed',
sortOrder: 'desc'
},
{
id: 'recently-added',
label: 'Recently added',
heading: 'Recently Added',
filters: { progress: ['unread'] },
orderBy: 'created_at',
sortOrder: 'desc'
},
{
id: 'discover',
label: 'Discover',
heading: 'Discover',
filters: { progress: ['unread'] },
sortOrder: 'random'
},
{
id: 'read-again',
label: 'Read again',
heading: 'Read Again',
filters: { progress: ['read'] },
sortOrder: 'random'
}
];
export function getPreset(id: string) {
return BOOK_PRESETS.find((preset) => preset.id === id);
}
/**
* Query string for the books endpoint, used by the home route's loader.
*/
export function presetQuery(preset: BookPreset, libraryId: string | number) {
const params = new URLSearchParams();
params.set('libraries', String(libraryId));
for (const [key, values] of Object.entries(preset.filters)) {
for (const value of values) params.append(key, value);
}
if (preset.orderBy) params.set('orderBy', preset.orderBy);
if (preset.sortOrder) params.set('sortOrder', preset.sortOrder);
return params.toString();
}