Files
bookhoard/templates/bookshelf_templ.go
T
john-okeefe 07abcabaa8 refactor: clean up tests and templates for media-items system
- Remove ebook-specific test files (ebook_test.go, integration_test.go, notes_highlights_test.go)
- Update search_test.go for media-items API paths
- Regenerate templates (bookshelf_templ.go, header_templ.go)
- Add ISBN normalization utility function
- Clean up test suite to focus on media-items functionality

Aligns tests and templates with unified media-items architecture
2026-01-30 13:52:11 -05:00

49 lines
18 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 BookShelf(user User) 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>Library - Bookmann</title><script src=\"/static/htmx.min.js\"></script><script src=\"/static/toast.js\"></script><script src=\"/static/header.js\"></script><script src=\"/static/search.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><style>\n :root {\n --bg-primary: #1a1b26;\n --bg-secondary: #16161e;\n --text-primary: #a9b1d6;\n --text-secondary: #565f89;\n --accent: #7aa2f7;\n --border: #414868;\n }\n .theme-tokyo-night {\n --bg-primary: #1a1b26;\n --bg-secondary: #16161e;\n --text-primary: #a9b1d6;\n --text-secondary: #565f89;\n --accent: #7aa2f7;\n --border: #414868;\n }\n .theme-dracula {\n --bg-primary: #282a36;\n --bg-secondary: #21222c;\n --text-primary: #f8f8f2;\n --text-secondary: #6272a4;\n --accent: #bd93f9;\n --border: #44475a;\n }\n .theme-nord {\n --bg-primary: #2e3440;\n --bg-secondary: #3b4252;\n --text-primary: #eceff4;\n --text-secondary: #5e81ac;\n --accent: #88c0d0;\n --border: #4c566a;\n }\n .theme-solarized-dark {\n --bg-primary: #002b36;\n --bg-secondary: #073642;\n --text-primary: #93a1a1;\n --text-secondary: #586e75;\n --accent: #2aa198;\n --border: #586e75;\n }\n .theme-monokai {\n --bg-primary: #272822;\n --bg-secondary: #3e3d32;\n --text-primary: #f8f8f2;\n --text-secondary: #75715e;\n --accent: #a6e22e;\n --border: #49483e;\n }\n .theme-one-dark-pro {\n --bg-primary: #282c34;\n --bg-secondary: #21252b;\n --text-primary: #abb2bf;\n --text-secondary: #5c6370;\n --accent: #61dafb;\n --border: #3e4451;\n }\n .theme-material-dark {\n --bg-primary: #263238;\n --bg-secondary: #37474f;\n --text-primary: #eeffff;\n --text-secondary: #546e7a;\n --accent: #80cbc4;\n --border: #455a64;\n }\n .theme-catppuccin-mocha {\n --bg-primary: #1e1e2e;\n --bg-secondary: #181825;\n --text-primary: #cdd6f4;\n --text-secondary: #bac2de;\n --accent: #f38ba8;\n --border: #313244;\n }\n .theme-catppuccin-macchiato {\n --bg-primary: #24273a;\n --bg-secondary: #1e2030;\n --text-primary: #cad3f5;\n --text-secondary: #b8c0e0;\n --accent: #f0c6c6;\n --border: #363a4f;\n }\n .theme-catppuccin-frappe {\n --bg-primary: #303446;\n --bg-secondary: #292c3c;\n --text-primary: #c6d0f5;\n --text-secondary: #b5bfe2;\n --accent: #f2d5cf;\n --border: #414559;\n }\n .theme-catppuccin-latte {\n --bg-primary: #eff1f5;\n --bg-secondary: #e6e9ef;\n --text-primary: #4c4f69;\n --text-secondary: #5c5f77;\n --accent: #d20f39;\n --border: #bcc0cc;\n }\n /* Wood themes */\n .theme-wood-light {\n --bg-primary: rgba(222, 184, 135, 0.9);\n --bg-secondary: rgba(210, 166, 121, 0.95);\n --text-primary: #2c1810;\n --text-secondary: #5a3a2a;\n --accent: #8b5a2b;\n --border: #6b4423;\n }\n .theme-wood-dark {\n --bg-primary: rgba(139, 115, 85, 0.9);\n --bg-secondary: rgba(107, 83, 68, 0.95);\n --text-primary: #f5e6d3;\n --text-secondary: #d4c4b0;\n --accent: #deb887;\n --border: #5a4636;\n }\n .theme-wood-mahogany {\n --bg-primary: rgba(160, 82, 45, 0.9);\n --bg-secondary: rgba(139, 69, 19, 0.95);\n --text-primary: #ffe4c4;\n --text-secondary: #daa520;\n --accent: #f4a460;\n --border: #8b4513;\n }\n body {\n background-color: var(--bg-primary);\n color: var(--text-primary);\n min-height: 100vh;\n transition: background 0.3s ease;\n }\n .card {\n background-color: var(--bg-secondary);\n border-color: var(--border);\n }\n .btn-primary {\n background-color: var(--accent);\n color: var(--bg-primary);\n }\n .shelf {\n position: relative;\n background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.1) 100%);\n padding: 2rem 1rem;\n margin-bottom: 1rem;\n border-radius: 0.5rem;\n }\n .shelf::after {\n content: '';\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 12px;\n background: linear-gradient(to bottom, \n rgba(107, 68, 35, 0.3) 0%, \n rgba(107, 68, 35, 0.5) 50%,\n rgba(107, 68, 35, 0.3) 100%);\n border-radius: 0 0 0.5rem 0.5rem;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n }\n .book-item {\n position: relative;\n transition: transform 0.2s ease, box-shadow 0.2s ease;\n cursor: pointer;\n }\n .book-item:hover {\n transform: translateY(-8px) rotate(-2deg);\n box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);\n z-index: 10;\n }\n .book-cover {\n aspect-ratio: 2/3;\n overflow: hidden;\n border-radius: 0.25rem;\n box-shadow: \n 2px 2px 4px rgba(0, 0, 0, 0.2),\n -1px -1px 2px rgba(255, 255, 255, 0.1) inset;\n position: relative;\n }\n .book-cover::before {\n content: '';\n position: absolute;\n left: 0;\n top: 0;\n bottom: 0;\n width: 4px;\n background: linear-gradient(to right,\n rgba(0, 0, 0, 0.3) 0%,\n rgba(255, 255, 255, 0.1) 50%,\n transparent 100%);\n }\n .library-selector {\n margin-bottom: 2rem;\n }\n .loading-spinner {\n display: inline-block;\n width: 40px;\n height: 40px;\n border: 4px solid var(--border);\n border-top-color: var(--accent);\n border-radius: 50%;\n animation: spin 1s linear infinite;\n }\n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n </style></head><body class=\"theme-tokyo-night\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = Header(user, "/bookshelf").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\"><!-- Library Selector --><div class=\"library-selector\"><div class=\"flex flex-col sm:flex-row gap-4 items-center justify-between\"><div class=\"flex-1 w-full\"><label class=\"block text-sm font-medium mb-2\" style=\"color: var(--text-secondary)\">Select Library</label> <select id=\"library-select\" onchange=\"selectLibrary()\" class=\"w-full px-4 py-3 border rounded-lg text-lg\" style=\"background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border);\"><option value=\"\">Loading libraries...</option></select></div><div class=\"flex gap-2\"><button onclick=\"loadBookshelf()\" class=\"btn-primary px-6 py-3 rounded-lg font-medium\">Refresh</button></div></div></div><!-- Bookshelf Container --><div id=\"bookshelf-container\"><!-- Loading state --><div id=\"loading\" class=\"text-center py-16\" style=\"color: var(--text-secondary)\"><div class=\"loading-spinner mx-auto mb-4\"></div><p>Loading your library...</p></div><!-- Empty state --><div id=\"empty-state\" class=\"hidden text-center py-16\" 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 Books Yet</h3><p>Select a library to view your collection</p></div><!-- Books Grid --><div id=\"books-grid\" class=\"hidden\"><!-- Books will be loaded here on shelves --></div></div></div><script src=\"/static/theme.js\"></script><script>\n let libraries = [];\n let currentLibrary = null;\n let mediaItems = [];\n\n function loadLibraries() {\n const loading = document.getElementById('loading');\n const librarySelect = document.getElementById('library-select');\n \n loading.style.display = 'block';\n\n fetch('/api/libraries/visible', {\n headers: {\n 'Authorization': 'Bearer ' + localStorage.getItem('token'),\n 'Content-Type': 'application/json'\n }\n })\n .then(response => response.json())\n .then(data => {\n loading.style.display = 'none';\n libraries = data;\n populateLibrarySelect();\n \n // Auto-select first library if available\n if (libraries.length > 0 && !currentLibrary) {\n currentLibrary = libraries[0];\n librarySelect.value = currentLibrary.id;\n loadBookshelf();\n }\n })\n .catch(error => {\n loading.style.display = 'none';\n console.error('Error loading libraries:', error);\n showToast('Error loading libraries', 'error');\n });\n }\n\n function populateLibrarySelect() {\n const librarySelect = document.getElementById('library-select');\n \n if (libraries.length === 0) {\n librarySelect.innerHTML = '<option value=\"\">No libraries available</option>';\n return;\n }\n\n librarySelect.innerHTML = '<option value=\"\">Select a library...</option>' +\n libraries.map(library => {\n const icon = getLibraryIcon(library.type_name);\n return `<option value=\"${library.id}\">${icon} ${library.name}</option>`;\n }).join('');\n }\n\n function getLibraryIcon(typeName) {\n switch (typeName) {\n case 'ebooks': return '📚';\n case 'comics': return '📖';\n case 'manga': return '🗾';\n default: return '📁';\n }\n }\n\n function selectLibrary() {\n const librarySelect = document.getElementById('library-select');\n const libraryId = librarySelect.value;\n \n currentLibrary = libraries.find(l => l.id === libraryId);\n loadBookshelf();\n }\n\n function loadBookshelf() {\n if (!currentLibrary) {\n showEmptyState();\n return;\n }\n\n const loading = document.getElementById('loading');\n const booksGrid = document.getElementById('books-grid');\n const emptyState = document.getElementById('empty-state');\n\n loading.style.display = 'block';\n booksGrid.classList.add('hidden');\n emptyState.classList.add('hidden');\n\n fetch(`/api/media-items?library_id=${currentLibrary.id}&limit=100&offset=0`, {\n headers: {\n 'Authorization': 'Bearer ' + localStorage.getItem('token'),\n 'Content-Type': 'application/json'\n }\n })\n .then(response => response.json())\n .then(data => {\n loading.style.display = 'none';\n mediaItems = data;\n \n if (mediaItems.length === 0) {\n showEmptyState();\n } else {\n renderBookshelf();\n }\n })\n .catch(error => {\n loading.style.display = 'none';\n console.error('Error loading bookshelf:', error);\n showToast('Error loading books', 'error');\n });\n }\n\n function showEmptyState() {\n const booksGrid = document.getElementById('books-grid');\n const emptyState = document.getElementById('empty-state');\n const loading = document.getElementById('loading');\n \n loading.style.display = 'none';\n booksGrid.classList.add('hidden');\n emptyState.classList.remove('hidden');\n }\n\n function renderBookshelf() {\n const booksGrid = document.getElementById('books-grid');\n const emptyState = document.getElementById('empty-state');\n const loading = document.getElementById('loading');\n \n loading.style.display = 'none';\n emptyState.classList.add('hidden');\n booksGrid.classList.remove('hidden');\n\n // Group books into shelves (6 books per shelf for better display)\n const booksPerShelf = 6;\n const shelves = [];\n \n for (let i = 0; i < mediaItems.length; i += booksPerShelf) {\n shelves.push(mediaItems.slice(i, i + booksPerShelf));\n }\n\n let html = '';\n shelves.forEach((shelfBooks, index) => {\n html += `<div class=\"shelf\">\n <div class=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4\">\n ${shelfBooks.map(book => renderBookCard(book)).join('')}\n </div>\n </div>`;\n });\n\n booksGrid.innerHTML = html;\n }\n\n function renderBookCard(book) {\n const coverUrl = book.cover_image_path || '/static/placeholder-book.svg';\n const authorHtml = book.author ? `<p class=\"text-xs\" style=\"color: var(--text-secondary)\">${book.author}</p>` : '';\n \n return `<div class=\"book-item\" onclick=\"viewBook('${book.id}')\">\n <div class=\"book-cover\">\n <img src=\"${coverUrl}\" \n alt=\"${book.title}\" \n class=\"w-full h-full object-cover\"\n onerror=\"this.src='/static/placeholder-book.svg'\"\n >\n </div>\n <div class=\"mt-2\">\n <h3 class=\"text-sm font-semibold line-clamp-2\" style=\"color: var(--text-primary)\">${book.title}</h3>\n ${authorHtml}\n </div>\n </div>`;\n }\n\n function viewBook(bookId) {\n // TODO: Navigate to book detail view\n showToast('Book viewer coming soon!', 'info');\n }\n\n // Initialize\n document.addEventListener('DOMContentLoaded', function() {\n loadTheme();\n loadLibraries();\n });\n </script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate