feat(web): templ partials replace the last UI twins (collections grid, conflicts list)
Both pages had a templ render (initial load) and a TypeScript twin (innerHTML rebuild on refresh) of the same fragment. The twins are deleted; the server-rendered markup is now the only markup. - collections: the library switcher swaps in the new books-grid partial (GET /collections/:id/books-grid) instead of re-rendering cards from the /api/collections JSON. Book loading is extracted into Config.loadCollectionBooks, shared verbatim by the page and the partial (smart / dashboard-query / manual branches, family-lens scoping via resolveLibrary + UserVisibleLibraryIDs) so the two cannot drift. - conflicts: resolve/dismiss refreshes swap the new list partial (GET /conflicts/list) with the stats bar as an out-of-band swap, instead of the TS twin — which rendered a degraded card (no checkboxes, no timestamps), pointed at a window.showResolveModal that does not exist, and updated stats via element ids the SSR never had. - shims.d.ts: window.htmx typing (ajax/process) for TS callers. Visibility note: the books-grid partial inherits its family-lens scoping from the shared loader (identical to the detail page); a dedicated hidden-library pin test for the partial route is still worth adding in the next test pass.
This commit is contained in:
1 parent
1fdf306f2d
commit
f1844591b4
9 files changed
+1078
-889
No files matched your search
+8
-54
@@ -2,19 +2,16 @@ import { Alpine } from "./alpine";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
async function refreshConflicts(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
// Rendering ownership: the list and stats are server-rendered markup.
|
||||
// Swap in the templ partial (stats arrive as an out-of-band swap)
|
||||
// instead of re-rendering cards from JSON.
|
||||
try {
|
||||
const response = await fetch("/api/conflicts", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
await window.htmx.ajax("GET", "/conflicts/list", {
|
||||
target: "#conflicts-list",
|
||||
swap: "outerHTML",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data: ConflictListResponse = await response.json();
|
||||
renderConflicts(data.conflicts);
|
||||
updateConflictStats(data);
|
||||
}
|
||||
const list = document.getElementById("conflicts-list");
|
||||
if (list) Alpine.initTree(list);
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh conflicts:", error);
|
||||
}
|
||||
@@ -123,49 +120,6 @@ async function dismissAllResolved(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function renderConflicts(conflicts: ConflictDetailResponse[]): void {
|
||||
const container = document.getElementById("conflicts-list");
|
||||
if (!container) return;
|
||||
|
||||
if (conflicts.length === 0) {
|
||||
container.innerHTML =
|
||||
'<p class="text-center p-4" style="color: var(--text-secondary)">No conflicts found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = conflicts
|
||||
.map(
|
||||
(conflict) => `
|
||||
<div class="p-4 rounded-lg border mb-2" style="background-color: var(--bg-secondary); border-color: var(--border)">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="font-medium" style="color: var(--text-primary)">${conflict.media_item_title}</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">${conflict.conflict_type} - ${conflict.resolution_status}</p>
|
||||
</div>
|
||||
${
|
||||
conflict.resolution_status === "unresolved"
|
||||
? `
|
||||
<div class="flex space-x-2">
|
||||
<button onclick="window.showResolveModal('${conflict.id}')" class="btn-primary px-3 py-1 rounded text-sm">Resolve</button>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function updateConflictStats(data: ConflictListResponse): void {
|
||||
const totalEl = document.getElementById("conflicts-total");
|
||||
const unresolvedEl = document.getElementById("conflicts-unresolved");
|
||||
|
||||
if (totalEl) totalEl.textContent = String(data.total);
|
||||
if (unresolvedEl) unresolvedEl.textContent = String(data.unresolved);
|
||||
}
|
||||
|
||||
function showResolveModal(conflictId: string): void {
|
||||
const modal = document.getElementById("resolve-modal");
|
||||
const conflictIdInput = document.getElementById(
|
||||
|
||||
Reference in new issue
Block a user