Phase 3 part 1: Add interactive API explorer - Create templates/api_explorer.templ with mock and real modes - Add EndpointInfo and APIExplorerData types to templates/types.go - Fix markdown rendering with UnsafeHTML.ToComponent() - Add templ import for Component support - API explorer supports: - Mock data mode for non-authenticated users - Real execution mode for logged-in users - Request/response display - cURL generation - Copy to clipboard
245 lines
34 KiB
Go
245 lines
34 KiB
Go
// Code generated by templ - DO NOT EDIT.
|
||
|
||
// templ: version: v0.3.977
|
||
package templates
|
||
|
||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||
|
||
import "github.com/a-h/templ"
|
||
import templruntime "github.com/a-h/templ/runtime"
|
||
|
||
func Collection(user User, collections []CollectionData) templ.Component {
|
||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||
return templ_7745c5c3_CtxErr
|
||
}
|
||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||
if !templ_7745c5c3_IsBuffer {
|
||
defer func() {
|
||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err == nil {
|
||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||
}
|
||
}()
|
||
}
|
||
ctx = templ.InitializeContext(ctx)
|
||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||
if templ_7745c5c3_Var1 == nil {
|
||
templ_7745c5c3_Var1 = templ.NopComponent
|
||
}
|
||
ctx = templ.ClearChildren(ctx)
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Collections - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><script src=\"/static/header.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Header(user, "/collections").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-8 flex justify-between items-center\"><div><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">My Collections</h1><p style=\"color: var(--text-secondary)\">Organize your books into custom collections</p></div><button onclick=\"showCreateModal()\" class=\"btn-primary px-4 py-2 rounded-lg\">➕ New Collection</button></div><div id=\"collections-container\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if len(collections) == 0 {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"text-center py-16 col-span-full\" style=\"color: var(--text-secondary)\"><div class=\"text-6xl mb-4\">📚</div><h3 class=\"text-xl font-semibold mb-2\" style=\"color: var(--text-primary)\">No Collections Yet</h3><p class=\"mb-4\">Create collections to organize your books</p><button onclick=\"showCreateModal()\" class=\"btn-primary px-4 py-2 rounded-lg\">Create Your First Collection</button></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
for _, col := range collections {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"card p-6 rounded-lg border cursor-pointer hover:shadow-lg transition-shadow\" style=\"background-color: var(--bg-secondary); border-color: { col.Color }; border-left-width: 4px; border-left-style: solid;\" onclick=\"viewCollection('{ col.ID }')\"><div class=\"flex justify-between items-start mb-4\"><div class=\"text-3xl\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var2 string
|
||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 46, Col: 60}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"flex space-x-2\"><button onclick=\"event.stopPropagation(); editCollection('{ col.ID }')\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-primary);\">✏️</button> <button onclick=\"event.stopPropagation(); deleteCollection('{ col.ID }')\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-secondary); background-color: var(--bg-primary);\">🗑️</button></div></div><h3 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var3 string
|
||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 58, Col: 109}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</h3><p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var4 string
|
||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 59, Col: 103}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div></div><div id=\"create-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-md mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Create Collection</h2><button onclick=\"hideCreateModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><form id=\"create-form\" onsubmit=\"handleCreate(event)\"><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Name</label> <input type=\"text\" id=\"collection-name\" required class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"My Reading List\"></div><div class=\"mb-4\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Description</label> <textarea id=\"collection-description\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\" placeholder=\"Optional description\" rows=\"3\"></textarea></div><div class=\"mb-6\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Color</label><div class=\"flex gap-2\"><button type=\"button\" onclick=\"selectColor('#7aa2f7')\" class=\"w-8 h-8 rounded-full color-option\" style=\"background-color: #7aa2f7;\"></button> <button type=\"button\" onclick=\"selectColor('#f7768e')\" class=\"w-8 h-8 rounded-full color-option\" style=\"background-color: #f7768e;\"></button> <button type=\"button\" onclick=\"selectColor('#e0af68')\" class=\"w-8 h-8 rounded-full color-option\" style=\"background-color: #e0af68;\"></button> <button type=\"button\" onclick=\"selectColor('#9ece6a')\" class=\"w-8 h-8 rounded-full color-option\" style=\"background-color: #9ece6a;\"></button> <button type=\"button\" onclick=\"selectColor('#7dcfff')\" class=\"w-8 h-8 rounded-full color-option\" style=\"background-color: #7dcfff;\"></button> <button type=\"button\" onclick=\"selectColor('#bb9af7')\" class=\"w-8 h-8 rounded-full color-option\" style=\"background-color: #bb9af7;\"></button></div><input type=\"hidden\" id=\"collection-color\" value=\"#7aa2f7\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"hideCreateModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"submit\" class=\"btn-primary px-4 py-2 rounded-lg\">Create Collection</button></div></form></div></div><script>\n let selectedColor = '#7aa2f7';\n\n function showCreateModal() {\n document.getElementById('create-modal').classList.remove('hidden');\n }\n\n function hideCreateModal() {\n document.getElementById('create-modal').classList.add('hidden');\n document.getElementById('create-form').reset();\n selectedColor = '#7aa2f7';\n updateColorSelection();\n }\n\n function selectColor(color) {\n selectedColor = color;\n document.getElementById('collection-color').value = color;\n updateColorSelection();\n }\n\n function updateColorSelection() {\n document.querySelectorAll('.color-option').forEach(btn => {\n btn.style.outline = 'none';\n btn.style.outlineOffset = '0';\n });\n const selectedBtn = document.querySelector(`.color-option[onclick=\"selectColor('${selectedColor}')\"]`);\n if (selectedBtn) {\n selectedBtn.style.outline = '3px solid var(--text-primary)';\n selectedBtn.style.outlineOffset = '3px';\n }\n }\n\n function handleCreate(event) {\n event.preventDefault();\n\n const data = {\n name: document.getElementById('collection-name').value,\n description: document.getElementById('collection-description').value,\n color: selectedColor,\n icon: '📚'\n };\n\n fetch('/api/collections', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify(data)\n })\n .then(response => response.json())\n .then(result => {\n showToast('Collection created successfully', 'success');\n hideCreateModal();\n location.reload();\n })\n .catch(error => {\n showToast('Failed to create collection', 'error');\n });\n }\n\n function viewCollection(id) {\n window.location.href = `/collections/${id}`;\n }\n\n function editCollection(id) {\n showToast('Edit feature coming soon!', 'info');\n }\n\n function deleteCollection(id) {\n if (!confirm('Are you sure you want to delete this collection?')) return;\n\n fetch(`/api/collections/${id}`, {\n method: 'DELETE',\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => {\n if (response.ok) {\n showToast('Collection deleted successfully', 'success');\n location.reload();\n } else {\n showToast('Failed to delete collection', 'error');\n }\n })\n .catch(error => {\n showToast('Failed to delete collection', 'error');\n });\n }\n\n function logout() {\n localStorage.removeItem('token');\n window.location.href = '/login';\n }\n </script></body></html>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func CollectionDetail(user User, collection CollectionDetailData, books []BookData) templ.Component {
|
||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||
return templ_7745c5c3_CtxErr
|
||
}
|
||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||
if !templ_7745c5c3_IsBuffer {
|
||
defer func() {
|
||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err == nil {
|
||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||
}
|
||
}()
|
||
}
|
||
ctx = templ.InitializeContext(ctx)
|
||
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||
if templ_7745c5c3_Var5 == nil {
|
||
templ_7745c5c3_Var5 = templ.NopComponent
|
||
}
|
||
ctx = templ.ClearChildren(ctx)
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var6 string
|
||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 219, Col: 32}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><script src=\"/static/header.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = Header(user, "/collections").Render(ctx, templ_7745c5c3_Buffer)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8\"><div class=\"mb-6\"><button onclick=\"backToCollections()\" class=\"btn-secondary px-4 py-2 rounded-lg mb-4\">← Back to Collections</button><div class=\"flex items-center gap-4\"><div class=\"text-4xl\" style=\"color: { collection.Color }\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var7 string
|
||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 234, Col: 95}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><div><h1 class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var8 string
|
||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 236, Col: 107}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</h1><p style=\"color: var(--text-secondary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var9 string
|
||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 237, Col: 88}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p></div></div></div><div class=\"mb-6 flex justify-between items-center\"><div class=\"flex items-center gap-4\"><h2 class=\"text-xl font-semibold\" style=\"color: var(--text-primary)\">Books in this Collection</h2><span id=\"selected-count\" class=\"hidden px-3 py-1 text-sm rounded\" style=\"background-color: var(--accent); color: var(--bg-primary);\">0 selected</span></div><div class=\"flex gap-3\"><div class=\"flex-1 max-w-md\"><input type=\"text\" id=\"collection-search\" placeholder=\"Search within collection...\" onkeyup=\"filterCollectionBooks()\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><button id=\"bulk-remove-btn\" onclick=\"removeSelectedBooks()\" disabled class=\"btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed\">🗑️ Remove Selected</button> <button onclick=\"showAddBooksModal()\" class=\"btn-primary px-4 py-2 rounded-lg\">➕ Add Books</button></div></div><div id=\"books-container\" class=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if len(books) == 0 {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div id=\"empty-state\" class=\"col-span-full text-center py-16\" style=\"color: var(--text-secondary)\">No books in this collection yet.</div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
for _, book := range books {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"card p-4 rounded-lg border hover:shadow-lg transition-shadow\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex items-start gap-4\"><input type=\"checkbox\" onchange=\"toggleBookForRemoval('{ book.MediaItemID }')\" class=\"w-5 h-5 mt-2\"><div class=\"aspect-w-3 aspect-h-4 flex-shrink-0 w-24 mb-3 overflow-hidden rounded\"><img src=\"{ book.CoverImagePath }\" alt=\"Cover\" class=\"w-full h-32 object-cover rounded\" onerror=\"this.src='/static/placeholder-book.svg'\"></div><div class=\"flex-1\"><h3 class=\"font-semibold text-lg mb-1 line-clamp-2\" style=\"color: var(--text-primary)\">")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var10 string
|
||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 284, Col: 131}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</h3>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
if book.Author != "" {
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<p class=\"text-sm\" style=\"color: var(--text-secondary)\">by ")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
var templ_7745c5c3_Var11 string
|
||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 286, Col: 108}
|
||
}
|
||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</p>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<button onclick=\"removeBook('{ book.MediaItemID }')\" class=\"mt-2 px-3 py-1 text-sm border rounded hover:opacity-80\" style=\"border-color: var(--border); color: var(--text-secondary);\">Remove</button></div></div></div>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
}
|
||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div></div><div id=\"add-books-modal\" class=\"hidden fixed inset-0 z-50 flex items-center justify-center\" style=\"background-color: rgba(0, 0, 0, 0.7);\"><div class=\"card rounded-lg p-6 w-full max-w-2xl mx-4\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"flex justify-between items-center mb-6\"><h2 class=\"text-xl font-bold\" style=\"color: var(--text-primary)\">Add Books to Collection</h2><button onclick=\"hideAddBooksModal()\" class=\"p-2 hover:opacity-80 rounded\" style=\"color: var(--text-primary)\">✕</button></div><p class=\"mb-4\" style=\"color: var(--text-secondary)\">Search and select books to add to this collection.</p><div class=\"mb-4\"><input type=\"text\" id=\"book-search\" placeholder=\"Search books...\" class=\"w-full px-4 py-2 border rounded-lg\" style=\"background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);\"></div><div id=\"book-results\" class=\"max-h-64 overflow-y-auto mb-4\"></div><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"hideAddBooksModal()\" class=\"btn-secondary px-4 py-2 rounded-lg\">Cancel</button> <button type=\"button\" onclick=\"addSelectedBooks()\" class=\"btn-primary px-4 py-2 rounded-lg\">Add Selected Books</button></div></div></div><script>\n let collectionId = '{ collection.ID }';\n let selectedBooks = new Set();\n let booksToRemove = new Set();\n let allBooks = [\n { range books }{\n {\n id: '{ book.MediaItemID }',\n title: '{ book.Title }',\n author: '{ book.Author }',\n cover: '{ book.CoverImagePath }'\n },\n }{ end }\n ];\n let ws = null;\n\n function backToCollections() {\n window.location.href = '/collections';\n }\n\n function connectWebSocket() {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const wsUrl = `${protocol}//${window.location.host}/ws/sync`;\n\n ws = new WebSocket(wsUrl);\n\n ws.onopen = function() {\n console.log('WebSocket connected');\n };\n\n ws.onmessage = function(event) {\n const message = JSON.parse(event.data);\n\n if (message.type === 'collection_updated' && message.data.collection_id === collectionId) {\n showToast(`Collection updated: ${message.data.action} (${message.data.count} books)`, 'info');\n\n setTimeout(() => {\n location.reload();\n }, 1000);\n }\n };\n\n ws.onclose = function() {\n console.log('WebSocket disconnected, reconnecting in 5s...');\n setTimeout(connectWebSocket, 5000);\n };\n\n ws.onerror = function(error) {\n console.error('WebSocket error:', error);\n };\n }\n\n connectWebSocket();\n\n function filterCollectionBooks() {\n const searchTerm = document.getElementById('collection-search').value.toLowerCase();\n const booksContainer = document.getElementById('books-container');\n const bookCards = booksContainer.children;\n\n let visibleCount = 0;\n for (let i = 0; i < bookCards.length; i++) {\n const card = bookCards[i];\n if (card.id === 'empty-state') continue;\n\n const title = card.querySelector('.font-semibold')?.textContent.toLowerCase() || '';\n const author = card.querySelector('.text-sm')?.textContent.toLowerCase() || '';\n\n const matches = title.includes(searchTerm) || author.includes(searchTerm);\n\n if (matches || searchTerm === '') {\n card.style.display = '';\n visibleCount++;\n } else {\n card.style.display = 'none';\n }\n }\n }\n\n function showAddBooksModal() {\n document.getElementById('add-books-modal').classList.remove('hidden');\n selectedBooks.clear();\n document.getElementById('book-results').innerHTML = '<p class=\"text-sm\" style=\"color: var(--text-secondary)\">Enter at least 2 characters to search.</p>';\n }\n\n function hideAddBooksModal() {\n document.getElementById('add-books-modal').classList.add('hidden');\n document.getElementById('book-search').value = '';\n document.getElementById('book-results').innerHTML = '';\n selectedBooks.clear();\n }\n\n function searchBooks() {\n const searchTerm = document.getElementById('book-search').value;\n const container = document.getElementById('book-results');\n\n if (searchTerm.length < 2) {\n container.innerHTML = '<p class=\"text-sm\" style=\"color: var(--text-secondary)\">Enter at least 2 characters to search.</p>';\n return;\n }\n\n container.innerHTML = '<p class=\"text-sm\" style=\"color: var(--text-secondary)\">Searching...</p>';\n\n fetch(`/api/media-items/search?q=${encodeURIComponent(searchTerm)}`, {\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => response.json())\n .then(result => {\n if (result.data && result.data.length > 0) {\n let html = '<div class=\"space-y-2\">';\n result.data.slice(0, 50).forEach(book => {\n const isSelected = selectedBooks.has(book.media_item_id);\n const checkedAttr = isSelected ? 'checked' : '';\n const authorHtml = book.author ? `<div class=\"text-xs\" style=\"color: var(--text-secondary)\">${book.author}</div>` : '';\n html += `\n <div class=\"flex items-center gap-3 p-2 rounded cursor-pointer hover:opacity-80\"\n style=\"background-color: var(--bg-primary);\"\n onclick=\"toggleBookSelection('${book.media_item_id}')\">\n <input type=\"checkbox\" ${checkedAttr} class=\"w-4 h-4\">\n <img src=\"${book.cover_image_path || '/static/placeholder-book.svg'}\"\n alt=\"Cover\" class=\"w-10 h-14 object-cover rounded\">\n <div class=\"flex-1\">\n <div class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">${book.title}</div>\n ${authorHtml}\n </div>\n </div>\n `;\n });\n html += '</div>';\n if (result.data.length > 50) {\n html += '<p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Showing first 50 of ' + result.data.length + ' results</p>';\n }\n container.innerHTML = html;\n } else {\n container.innerHTML = '<p class=\"text-sm\" style=\"color: var(--text-secondary)\">No books found</p>';\n }\n })\n .catch(error => {\n container.innerHTML = '<p class=\"text-sm\" style=\"color: var(--error)\">Failed to search books</p>';\n });\n }\n\n function toggleBookSelection(bookId) {\n if (selectedBooks.has(bookId)) {\n selectedBooks.delete(bookId);\n } else {\n selectedBooks.add(bookId);\n }\n searchBooks();\n }\n\n function addSelectedBooks() {\n if (selectedBooks.size === 0) {\n showToast('Please select at least one book', 'error');\n return;\n }\n\n const bookIds = Array.from(selectedBooks);\n\n fetch(`/api/collections/${collectionId}/books`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify({ book_ids: bookIds })\n })\n .then(response => {\n if (response.ok) {\n showToast(`Added ${bookIds.length} book(s) to collection`, 'success');\n hideAddBooksModal();\n location.reload();\n } else {\n showToast('Failed to add books', 'error');\n }\n })\n .catch(error => {\n showToast('Failed to add books', 'error');\n });\n }\n\n function removeBook(bookId) {\n if (!confirm('Remove this book from the collection?')) return;\n\n fetch(`/api/collections/${collectionId}/books/${bookId}`, {\n method: 'DELETE',\n headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }\n })\n .then(response => {\n if (response.ok) {\n showToast('Book removed from collection', 'success');\n location.reload();\n } else {\n showToast('Failed to remove book', 'error');\n }\n })\n .catch(error => {\n showToast('Failed to remove book', 'error');\n });\n }\n\n function toggleBookForRemoval(bookId) {\n if (booksToRemove.has(bookId)) {\n booksToRemove.delete(bookId);\n } else {\n booksToRemove.add(bookId);\n }\n updateSelectedCount();\n }\n\n function updateSelectedCount() {\n const count = booksToRemove.size;\n const countSpan = document.getElementById('selected-count');\n const removeBtn = document.getElementById('bulk-remove-btn');\n\n if (count > 0) {\n countSpan.textContent = count + ' selected';\n countSpan.classList.remove('hidden');\n removeBtn.disabled = false;\n } else {\n countSpan.classList.add('hidden');\n removeBtn.disabled = true;\n }\n }\n\n function removeSelectedBooks() {\n if (booksToRemove.size === 0) {\n showToast('No books selected', 'error');\n return;\n }\n\n if (!confirm(`Remove ${booksToRemove.size} book(s) from the collection?`)) return;\n\n const bookIds = Array.from(booksToRemove);\n\n fetch(`/api/collections/${collectionId}/books/bulk-remove`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + localStorage.getItem('token')\n },\n body: JSON.stringify({ book_ids: bookIds })\n })\n .then(response => response.json())\n .then(result => {\n if (result.removed > 0) {\n showToast(`Removed ${result.removed} book(s) from collection`, 'success');\n location.reload();\n } else {\n showToast('Failed to remove books', 'error');\n }\n })\n .catch(error => {\n showToast('Failed to remove books', 'error');\n });\n }\n })\n .catch(error => {\n showToast('Failed to remove book', 'error');\n });\n }\n\n function logout() {\n localStorage.removeItem('token');\n window.location.href = '/login';\n }\n\n renderBooks();\n </script></body></html>")
|
||
if templ_7745c5c3_Err != nil {
|
||
return templ_7745c5c3_Err
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
var _ = templruntime.GeneratedTemplate
|