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
244 lines
12 KiB
Templ
244 lines
12 KiB
Templ
package templates
|
|
|
|
import "bookhoard/internal/handlers"
|
|
|
|
templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int, unresolved int, errorMessage string) {
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Sync Conflicts - Bookhoard</title>
|
|
<script src="/static/htmx.min.js"></script>
|
|
<link href="/static/style.css" rel="stylesheet"/>
|
|
</head>
|
|
<body x-data="conflicts" class="theme-{ user.Theme }">
|
|
@Header(user, "/conflicts")
|
|
<div class="w-full px-4 sm:px-6 lg:px-8 py-8">
|
|
<div class="mb-8">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h1 class="text-3xl font-bold" style="color: var(--text-primary)">Sync Conflicts</h1>
|
|
<p style="color: var(--text-secondary)">Resolve conflicts when reading progress differs across devices</p>
|
|
</div>
|
|
<div class="flex space-x-3">
|
|
<select
|
|
id="status-filter"
|
|
onchange="filterConflicts()"
|
|
class="px-4 py-2 border rounded-lg"
|
|
style="background-color: var(--bg-secondary); color: var(--text-primary); border-color: var(--border);"
|
|
>
|
|
<option value="unresolved">Unresolved Only</option>
|
|
<option value="all">All Conflicts</option>
|
|
<option value="resolved">Resolved Only</option>
|
|
</select>
|
|
<button @click="dismissAllResolved()" class="btn-secondary px-4 py-2 rounded-lg">
|
|
Dismiss All Resolved
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="stats-bar" class="mt-4 flex space-x-6 text-sm">
|
|
<span style="color: var(--text-secondary)">Total: <strong style="color: var(--text-primary)">{ total }</strong></span>
|
|
<span style="color: var(--text-secondary)">Unresolved: <strong style="color: #f59e0b;">{ unresolved }</strong></span>
|
|
<span style="color: var(--text-secondary)">Resolved: <strong style="color: #10b981;">{ total - unresolved }</strong></span>
|
|
</div>
|
|
<div id="bulk-actions" class="mt-4 hidden">
|
|
<div class="flex items-center justify-between p-4 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="flex items-center space-x-4">
|
|
<span class="text-sm font-semibold" style="color: var(--text-primary);">
|
|
<span id="selected-count">0</span> conflicts selected
|
|
</span>
|
|
<select
|
|
id="bulk-strategy"
|
|
class="px-3 py-2 border rounded-lg text-sm"
|
|
style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border);"
|
|
>
|
|
<option value="most_recent">Most Recent</option>
|
|
<option value="highest_progress">Highest Progress</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<button @click="bulkResolve()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
|
✅ Resolve Selected
|
|
</button>
|
|
<button @click="bulkDismiss()" class="btn-danger px-4 py-2 rounded-lg text-sm">
|
|
❌ Dismiss Selected
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="loading" class="hidden text-center py-16" style="color: var(--text-secondary)">
|
|
<div class="loading-spinner mx-auto mb-4"></div>
|
|
<p>Loading conflicts...</p>
|
|
</div>
|
|
<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 Conflicts</h3>
|
|
<p>Your devices are in sync! No conflicts to resolve.</p>
|
|
</div>
|
|
<div id="conflicts-list" class="space-y-6">
|
|
<div id="select-all-container" class="mb-4 hidden">
|
|
<label class="flex items-center space-x-2 cursor-pointer">
|
|
<input type="checkbox" id="select-all-conflicts" class="w-5 h-5 rounded" onchange="toggleAllConflicts()"/>
|
|
<span class="text-sm font-semibold" style="color: var(--text-primary);">Select All</span>
|
|
</label>
|
|
</div>
|
|
if len(conflicts) == 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)">No Conflicts</h3>
|
|
<p>Your devices are in sync! No conflicts to resolve.</p>
|
|
</div>
|
|
}
|
|
for _, conflict := range conflicts {
|
|
<div class="conflict-item card p-6 rounded-lg border" data-conflict-id="{ conflict.ID }" style="background-color: var(--bg-secondary); border-color: var(--border); border-left-width: 4px; border-left-style: solid; border-left-color: rgb(245, 158, 11);">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<div class="flex items-start space-x-4">
|
|
<input
|
|
type="checkbox"
|
|
class="conflict-checkbox w-5 h-5 rounded mt-1"
|
|
data-conflict-id="{ conflict.ID }"
|
|
onchange="updateBulkActions()"
|
|
/>
|
|
<div>
|
|
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">{ conflict.MediaItemTitle }</h3>
|
|
<p class="text-sm" style="color: var(--text-secondary)">Type: { conflict.ConflictType }</p>
|
|
</div>
|
|
</div>
|
|
<button @click="showResolveModal('{ conflict.ID }')" class="btn-primary px-4 py-2 rounded-lg">
|
|
Resolve
|
|
</button>
|
|
</div>
|
|
<div class="text-sm" style="color: var(--text-secondary)">
|
|
Created: { conflict.CreatedAt.Format("2006-01-02 15:04:05") }
|
|
if conflict.ResolvedBy != "" {
|
|
| Resolved by: { conflict.ResolvedBy }
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div id="conflict-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center overflow-y-auto" style="background-color: rgba(0, 0, 0, 0.7);">
|
|
<div class="card rounded-lg p-6 w-full max-w-4xl mx-4 my-8" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h2 class="text-2xl font-bold" style="color: var(--text-primary)">Resolve Conflict</h2>
|
|
<p class="text-sm" id="conflict-book-title" style="color: var(--text-secondary);">Book Title</p>
|
|
</div>
|
|
<button @click="hideResolveModal()" class="p-2 hover:opacity-80 rounded-lg" style="color: var(--text-primary); background-color: var(--bg-primary);">
|
|
X
|
|
</button>
|
|
</div>
|
|
<form id="conflict-resolution-form" @submit="handleResolveSubmit($event)">
|
|
<input type="hidden" id="conflict-id"/>
|
|
<div id="conflict-details" class="mb-6"></div>
|
|
<div class="card p-4 rounded-lg border mb-6" style="background-color: var(--bg-primary); border-color: var(--border);">
|
|
<h3 class="font-semibold mb-4" style="color: var(--text-primary)">Resolution Options</h3>
|
|
<div class="space-y-3 mb-6">
|
|
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);">
|
|
<input type="radio" name="winner" value="koreader" class="mr-3" required/>
|
|
<div class="flex-1">
|
|
<span class="font-semibold" style="color: var(--text-primary);">Keep KOReader Progress</span>
|
|
<p class="text-xs" style="color: var(--text-secondary);">Use progress from your KOReader device</p>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);">
|
|
<input type="radio" name="winner" value="kobo" class="mr-3"/>
|
|
<div class="flex-1">
|
|
<span class="font-semibold" style="color: var(--text-primary);">Keep Kobo Progress</span>
|
|
<p class="text-xs" style="color: var(--text-secondary);">Use progress from your Kobo device</p>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);">
|
|
<input type="radio" name="winner" value="web" class="mr-3"/>
|
|
<div class="flex-1">
|
|
<span class="font-semibold" style="color: var(--text-primary);">Keep Web Progress</span>
|
|
<p class="text-xs" style="color: var(--text-secondary);">Use progress from the web interface</p>
|
|
</div>
|
|
</label>
|
|
<label class="flex items-center p-3 border rounded-lg cursor-pointer hover:opacity-80" style="border-color: var(--border);">
|
|
<input type="radio" name="winner" value="manual" class="mr-3"/>
|
|
<div class="flex-1">
|
|
<span class="font-semibold" style="color: var(--text-primary);">Manual Override</span>
|
|
<p class="text-xs" style="color: var(--text-secondary);">Specify a custom progress value</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<div id="manual-override" class="hidden mb-4 p-4 border rounded-lg" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
|
<h4 class="font-semibold mb-3" style="color: var(--text-primary);">Manual Progress</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Percentage (0-100)</label>
|
|
<input
|
|
type="number"
|
|
id="manual-percentage"
|
|
min="0"
|
|
max="100"
|
|
step="0.01"
|
|
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>
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Page Number</label>
|
|
<input
|
|
type="number"
|
|
id="manual-page"
|
|
min="1"
|
|
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>
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">EPUB CFI</label>
|
|
<input
|
|
type="text"
|
|
id="manual-epubcfi"
|
|
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>
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Chapter</label>
|
|
<input
|
|
type="number"
|
|
id="manual-chapter"
|
|
min="1"
|
|
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>
|
|
</div>
|
|
<div class="mb-6">
|
|
<label class="flex items-center space-x-3 cursor-pointer">
|
|
<input type="checkbox" id="apply-to-all" class="w-5 h-5 rounded"/>
|
|
<span style="color: var(--text-primary);">Apply this resolution to all future conflicts</span>
|
|
</label>
|
|
</div>
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-medium mb-2" style="color: var(--text-secondary);">Resolution Reason (optional)</label>
|
|
<input
|
|
type="text"
|
|
id="resolution-reason"
|
|
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="e.g., Device was offline, using most recent progress"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end space-x-3">
|
|
<button type="button" @click="hideResolveModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button>
|
|
<button type="button" @click="deleteConflict()" class="btn-danger px-4 py-2 rounded-lg">Dismiss</button>
|
|
<button type="submit" class="btn-primary px-6 py-2 rounded-lg">Resolve Conflict</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@ErrorToast(errorMessage)
|
|
</body>
|
|
</html>
|
|
}
|