Remove redundant <script src="/static/main.js" defer></script> tags from 17+ templates that include the @Header component, eliminating duplicate script loading that was causing Alpine.js to initialize twice per page load. The header.templ component now serves as the single source of truth for main.js inclusion, following the DRY principle and ensuring consistent script loading across all pages that use the header navigation. Additionally, add type="button" attribute to all buttons in header navigation to prevent default form submission behavior when buttons are clicked. Changes: - Remove main.js script tag from templates using @Header component - Keep main.js in header.templ (line 279) as universal inclusion point - Preserve main.js in special pages: index.templ, login.templ, register.templ (these don't use @Header and are standalone entry points) - Add type="button" to theme toggle, theme selection, wood paneling, and user menu buttons to prevent unwanted form submissions or page navigation Benefits: - Eliminates Alpine.js double-initialization bug - Reduces HTTP requests (one script load instead of two) - Improves maintainability (add header, get scripts automatically) - Fixes broken @click handlers on collections, devices, and other pages - Prevents buttons from triggering default form submission behavior Technical notes: - Templates affected: admin, analytics, bookshelf, collection_rules, collections, conflicts, custom_section, dashboard, devices, docs, library, profile, progress, queue, unlinked_books - No changes to entry pages (index, login, register) which don't use @Header - HTMX script remains in individual templates (stateless, no double-load issue) - All interactive buttons in header now explicitly marked type="button" to prevent default browser form submission behavior Related to: previous commit fixing Vite code-splitting
196 lines
8.2 KiB
Templ
196 lines
8.2 KiB
Templ
package templates
|
|
|
|
templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Unlinked Books - Bookhoard</title>
|
|
<script src="/static/htmx.min.js"></script>
|
|
<link href="/static/style.css" rel="stylesheet"/>
|
|
</head>
|
|
<body class="theme-{ user.Theme }" x-data="unlinkedBooks" x-init="setupEventDelegation()">
|
|
@Header(user, "/unlinked")
|
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Unlinked Books</h1>
|
|
<p style="color: var(--text-secondary)">Resolve books that couldn't be automatically matched between devices</p>
|
|
</div>
|
|
if len(unlinkedBooks) > 0 {
|
|
<div class="flex justify-between items-center mb-6 p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="flex items-center gap-4">
|
|
<input type="checkbox" id="select-all-unlinked" @change="toggleAllUnlinked()" class="w-5 h-5"/>
|
|
<label for="select-all-unlinked" class="cursor-pointer" style="color: var(--text-primary)">Select All</label>
|
|
<span id="selected-count" class="text-sm" style="color: var(--text-secondary)">0 selected</span>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button @click="bulkAutoLink()" class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
|
🤖 Auto-Link Selected (High Confidence)
|
|
</button>
|
|
<button @click="bulkGetSuggestions()" class="btn-secondary px-4 py-2 rounded-lg text-sm">
|
|
💡 Get Suggestions for Selected
|
|
</button>
|
|
<button @click="showBulkManualLink()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
|
🔗 Bulk Manual Link
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
<div id="unlinked-container" class="space-y-6">
|
|
if len(unlinkedBooks) == 0 {
|
|
<div class="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)">All Books Linked!</h3>
|
|
<p>There are no unlinked books that need manual resolution.</p>
|
|
</div>
|
|
}
|
|
for _, book := range unlinkedBooks {
|
|
<div class="card p-6 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="flex gap-6">
|
|
<div class="flex items-start pt-2">
|
|
<input type="checkbox" class="unlinked-checkbox w-5 h-5" data-progress-id="{ book.ProgressID }" data-title="{ book.TitleFromDevice }" onchange="updateSelectedCount()"/>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="text-4xl">
|
|
if book.DeviceType == "koreader" {
|
|
📖
|
|
} else if book.DeviceType == "kobo" {
|
|
📚
|
|
} else {
|
|
📱
|
|
}
|
|
</div>
|
|
<div>
|
|
<h3 class="font-semibold text-lg" style="color: var(--text-primary)">{ book.TitleFromDevice }</h3>
|
|
<p class="text-sm" style="color: var(--text-secondary)">
|
|
Device: { book.DeviceName } ({ book.DeviceType })
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-2 text-sm mb-4">
|
|
<div>
|
|
<span style="color: var(--text-secondary)">File Path:</span>
|
|
<code style="color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px;">
|
|
{ book.FilePath }
|
|
</code>
|
|
</div>
|
|
if book.SHA256 != "" {
|
|
<div>
|
|
<span style="color: var(--text-secondary)">SHA-256:</span>
|
|
<code style="color: var(--text-primary); background-color: var(--bg-primary); padding: 2px 6px; border-radius: 4px; font-size: 11px;">
|
|
{ book.SHA256 }
|
|
</code>
|
|
</div>
|
|
}
|
|
<div>
|
|
<span style="color: var(--text-secondary)">Last Synced:</span>
|
|
<span style="color: var(--text-primary)">{ book.LastSyncTime }</span>
|
|
</div>
|
|
if book.ConfidenceScore > 0 {
|
|
<div>
|
|
<span style="color: var(--text-secondary)">Auto-Link Confidence:</span>
|
|
<span style="color: var(--text-primary)">
|
|
{ book.ConfidenceScore }% confidence
|
|
</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="mb-4">
|
|
<button
|
|
@click="searchMatches('{ book.ProgressID }', '{ book.SHA256 }', '{ book.TitleFromDevice }')"
|
|
class="btn-secondary px-4 py-2 rounded-lg text-sm"
|
|
>
|
|
🔍 Find Potential Matches
|
|
</button>
|
|
<button
|
|
@click="showManualLinkModal('{ book.ProgressID }', '{ book.TitleFromDevice }')"
|
|
class="btn-primary px-4 py-2 rounded-lg text-sm"
|
|
>
|
|
🔗 Manually Link
|
|
</button>
|
|
</div>
|
|
<div id="matches-{ book.ProgressID }" class="hidden">
|
|
<div class="border-t pt-4" style="border-color: var(--border);">
|
|
<h4 class="font-semibold mb-3" style="color: var(--text-primary)">Potential Matches</h4>
|
|
<div id="matches-list-{ book.ProgressID }" class="space-y-3">
|
|
<p style="color: var(--text-secondary)">Click "Find Potential Matches" to search</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
<!-- Manual Link Modal -->
|
|
<div id="manual-link-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)">Manually Link Book</h2>
|
|
<button @click="hideManualLinkModal()" class="p-2 hover:opacity-80 rounded" style="color: var(--text-primary)">✕</button>
|
|
</div>
|
|
<input type="hidden" id="link-progress-id"/>
|
|
<input type="hidden" id="link-book-sha256"/>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Book Title from Device</label>
|
|
<input
|
|
type="text"
|
|
id="link-book-title"
|
|
readonly
|
|
class="w-full px-4 py-2 border rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-secondary); border-color: var(--border);"
|
|
/>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Search for Book to Link</label>
|
|
<div class="flex gap-2">
|
|
<input
|
|
type="text"
|
|
id="link-search-input"
|
|
placeholder="Search by title, author..."
|
|
class="flex-1 px-4 py-2 border rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
|
/>
|
|
<button
|
|
type="button"
|
|
@click="searchBooksForLink()"
|
|
class="btn-primary px-4 py-2 rounded-lg"
|
|
>
|
|
Search
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="link-search-results" class="mb-4 max-h-48 overflow-y-auto space-y-2">
|
|
<p style="color: var(--text-secondary)">Search for books to link</p>
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary)">Confidence Score</label>
|
|
<input
|
|
type="number"
|
|
id="link-confidence"
|
|
min="0"
|
|
max="1"
|
|
step="0.1"
|
|
value="1.0"
|
|
class="w-full px-4 py-2 border rounded-lg"
|
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
|
/>
|
|
<p class="text-xs mt-1" style="color: var(--text-secondary)">1.0 = manual override (highest confidence)</p>
|
|
</div>
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" @click="hideManualLinkModal()" class="btn-secondary px-4 py-2 rounded-lg">
|
|
Cancel
|
|
</button>
|
|
<button type="button" @click="confirmManualLink()" class="btn-primary px-4 py-2 rounded-lg">
|
|
Link Book
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
}
|