Update all page templates to accept an optional error message parameter and display it via the ErrorToast component when data loading fails.
240 lines
16 KiB
Templ
240 lines
16 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>
|
|
<script src="/static/toast.js"></script>
|
|
<link href="/static/style.css" rel="stylesheet">
|
|
<script src="/static/theme.js"></script>
|
|
</head>
|
|
<body class="theme-{ user.Theme }">
|
|
@Header(user, "/conflicts")
|
|
|
|
<div class="max-w-7xl mx-auto 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 onclick="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 onclick="bulkResolve()" class="btn-primary px-4 py-2 rounded-lg text-sm">
|
|
✅ Resolve Selected
|
|
</button>
|
|
<button onclick="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 onclick="showConflictModal('{ 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 onclick="hideConflictModal()" 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" onsubmit="handleResolveConflict(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" onclick="hideConflictModal()" class="btn-secondary px-4 py-2 rounded-lg">Cancel</button>
|
|
<button type="button" onclick="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>
|
|
|
|
<script src="/static/conflicts.js"></script>
|
|
|
|
@ErrorToast(errorMessage)
|
|
</body>
|
|
</html>
|
|
}
|