feat(filters): save current filter as smart collection — the bridge (item 3.5)

The bookshelf's save modal grows a second action: "Save as Collection"
POSTs the current (completed, scope-stamped) tree to
/api/collections — the same validated write path every tree
collection uses — and lands you on the new collection's page. The qf
engine then drives it everywhere: the shelf's live shelf-count, the
collections page, the replay chain for episode trees. The legacy
"Save Filter" (bookmark the wire params) stays as the sibling action.

This is the plan's bridge: a book search graduated into a live shelf
with one click, authored in the ONE filter system.
This commit is contained in:
John O'Keefe committed 2026-10-30 19:25:02 -04:00
1 parent 4f71eda6be
commit 94bd69d22d
3 files changed
+47 -3

No files matched your search

+39
View File
@@ -179,6 +179,45 @@ Alpine.data("bookshelf", () => ({
// Show success toast
showToast("Filter saved successfully", "success");
},
// Item 3.5 bridge: the current tree becomes a live smart collection.
// The qf engine then drives it everywhere — shelf, collections page,
// play chains — with zero re-authoring.
async saveFilterAsCollection() {
const token = localStorage.getItem("token");
if (!token) {
showToast("Not authenticated", "error");
return;
}
const nameInput = document.getElementById("save-filter-name") as HTMLInputElement | null;
const name = nameInput?.value?.trim() ?? "";
if (!name) {
showToast("Give the collection a name first", "error");
return;
}
const builder = builderData();
const tree = builder?.scopedTree?.();
if (!tree) {
showToast("Add a complete filter rule first", "error");
return;
}
try {
const response = await fetch("/api/collections", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({ name, filter_tree: tree }),
});
if (response.ok) {
this.showSaveModal = false;
showToast("Saved as a smart collection", "success");
window.location.href = `/collections/${((await response.json()) as { id: string }).id}`;
} else {
const body = (await response.json().catch(() => ({}))) as { error?: string };
showToast(body.error || "Failed to save the collection", "error");
}
} catch {
showToast("Failed to save the collection", "error");
}
},
// Load a saved filter into the form
async loadFilter(event: Event) {
const button = event.target as HTMLElement;