Saved Filters
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -138,11 +138,11 @@ func BookShelf(
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
- templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(uuidToString(filter.ID))
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(uuidToString(filter.ID))
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 277, Col: 173}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 296, Col: 173}
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -153,7 +153,7 @@ func BookShelf(
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(filter.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 279, Col: 27}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 298, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -194,7 +194,7 @@ func BookShelf(
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 313, Col: 59}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 332, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -227,7 +227,7 @@ func BookShelf(
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(offset/limit + 1)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 332, Col: 32}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 351, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
diff --git a/web/src/bookshelf.ts b/web/src/bookshelf.ts
index 9f7fd99..30438d2 100644
--- a/web/src/bookshelf.ts
+++ b/web/src/bookshelf.ts
@@ -2,6 +2,7 @@
import { Alpine } from "./alpine";
import { showToast } from "./toast";
+import { searchTagSuggestions, type TagSuggestion } from "./tag-dropdown";
function clearFilters(): void {
// Clear all form state (reuses helper)
@@ -82,6 +83,9 @@ Alpine.data("bookshelf", () => ({
hasCoverState: null as boolean | null,
dropdownAlign: "right" as "left" | "right",
resizeTimeout: null as number | null,
+ tagSuggestions: [] as TagSuggestion[],
+ showTagDropdown: false,
+ tagHighlightIndex: -1,
// Standalone function references (don't access component state)
clearFilters,
@@ -382,9 +386,53 @@ Alpine.data("bookshelf", () => ({
await this.fetchFieldValues("author", input.value, "author-datalist");
},
- // Fetch tag values for autocomplete
+ // Fetch tag values for autocomplete (custom dropdown)
async fetchTagValues(input: HTMLInputElement): Promise {
- await this.fetchFieldValues("tags", input.value, "tags-datalist");
+ const libraryId = (document.getElementById("library-select") as HTMLSelectElement)?.value;
+ if (!libraryId || input.value.length < 2) {
+ this.tagSuggestions = [];
+ this.showTagDropdown = false;
+ return;
+ }
+ const results = await searchTagSuggestions(input.value, libraryId);
+ this.tagSuggestions = results;
+ this.showTagDropdown = results.length > 0;
+ this.tagHighlightIndex = -1;
+ },
+
+ selectTagSuggestion(tag: string) {
+ const input = document.querySelector('input[name="tags_filter"]') as HTMLInputElement;
+ if (input) input.value = tag;
+ this.showTagDropdown = false;
+ this.tagSuggestions = [];
+ this.tagHighlightIndex = -1;
+ },
+
+ hideTagDropdown() {
+ setTimeout(() => { this.showTagDropdown = false; this.tagHighlightIndex = -1; }, 200);
+ },
+
+ onTagFilterKeydown(event: KeyboardEvent) {
+ if (event.key === "ArrowDown") {
+ event.preventDefault();
+ if (this.showTagDropdown && this.tagSuggestions.length > 0) {
+ this.tagHighlightIndex = (this.tagHighlightIndex + 1) % this.tagSuggestions.length;
+ }
+ } else if (event.key === "ArrowUp") {
+ event.preventDefault();
+ if (this.showTagDropdown && this.tagSuggestions.length > 0) {
+ this.tagHighlightIndex = this.tagHighlightIndex <= 0
+ ? this.tagSuggestions.length - 1
+ : this.tagHighlightIndex - 1;
+ }
+ } else if (event.key === "Enter" && this.tagHighlightIndex >= 0 && this.showTagDropdown) {
+ event.preventDefault();
+ this.selectTagSuggestion(this.tagSuggestions[this.tagHighlightIndex].value);
+ } else if (event.key === "Escape") {
+ this.showTagDropdown = false;
+ this.tagSuggestions = [];
+ this.tagHighlightIndex = -1;
+ }
},
// // Fetch genre values for autocomplete
diff --git a/web/src/tag-dropdown.ts b/web/src/tag-dropdown.ts
new file mode 100644
index 0000000..6791081
--- /dev/null
+++ b/web/src/tag-dropdown.ts
@@ -0,0 +1,28 @@
+export interface TagSuggestion {
+ value: string;
+ count: number;
+}
+
+export async function searchTagSuggestions(
+ query: string,
+ libraryId: string,
+): Promise {
+ if (!query || query.length < 2 || !libraryId) return [];
+
+ const token = localStorage.getItem("token");
+ if (!token) return [];
+
+ try {
+ const response = await fetch(
+ `/api/media-items/search?tags=${encodeURIComponent(query)}&library_id=${libraryId}&limit=20`,
+ { headers: { Authorization: `Bearer ${token}` } },
+ );
+
+ if (!response.ok) return [];
+
+ const data = await response.json();
+ return (data.results || []) as TagSuggestion[];
+ } catch {
+ return [];
+ }
+}