feat(web): update frontend TypeScript modules and API types
This commit updates the web frontend TypeScript modules: Core modules: - admin.ts: Admin panel functionality and user management - analytics.ts: Analytics dashboard and data visualization - api-explorer.ts: Interactive API documentation explorer - api.ts: Core API client with request/response handling - collections.ts: Book collection management UI - conflicts.ts: Sync conflict resolution interface - custom-section-builder.ts: Dynamic section builder for UI - docs.ts: Documentation viewer and navigation - dom.ts: DOM manipulation utilities and helpers - header.ts: Application header with navigation - library.ts: Library view and book grid management - linking.ts: Device-book linking interface - password_validation.ts: Client-side password strength validation - queue.ts: Device sync queue management UI - search.ts: Full-text search with Lunr integration - storage.ts: Local storage and cache management - theme.ts: Theme management and CSS variable updates - themeDropdown.ts: Theme selector dropdown component - toast.ts: Toast notification system - woodPaneling.ts: Visual theme effects - woodPanelingInit.ts: Visual effects initialization Type definitions: - api.d.ts: Updated TypeScript definitions for API responses These updates enhance the frontend with improved functionality for book management, device synchronization, and user experience.
This commit is contained in:
+164
-140
@@ -1,203 +1,227 @@
|
||||
async function refreshConflicts(): Promise<void> {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/conflicts', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/conflicts", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data: ConflictListResponse = await response.json();
|
||||
renderConflicts(data.conflicts);
|
||||
updateConflictStats(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh conflicts:', error);
|
||||
if (response.ok) {
|
||||
const data: ConflictListResponse = await response.json();
|
||||
renderConflicts(data.conflicts);
|
||||
updateConflictStats(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to refresh conflicts:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveConflict(conflictId: string, winner: string, manualData?: Record<string, unknown>): Promise<void> {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
async function resolveConflict(
|
||||
conflictId: string,
|
||||
winner: string,
|
||||
manualData?: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/conflicts/${conflictId}/resolve`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ winner, manual_data: manualData })
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`/api/conflicts/${conflictId}/resolve`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ winner, manual_data: manualData }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success('Conflict resolved');
|
||||
}
|
||||
refreshConflicts();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error(error.error || 'Failed to resolve conflict');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to resolve conflict:', error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error('Failed to resolve conflict');
|
||||
}
|
||||
if (response.ok) {
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success("Conflict resolved");
|
||||
}
|
||||
refreshConflicts();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error(
|
||||
error.error || "Failed to resolve conflict",
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to resolve conflict:", error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error("Failed to resolve conflict");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkResolve(strategy: 'most_recent' | 'highest_progress', conflictIds: string[]): Promise<void> {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
async function bulkResolve(
|
||||
strategy: "most_recent" | "highest_progress",
|
||||
conflictIds: string[],
|
||||
): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/conflicts/bulk-resolve', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ conflict_ids: conflictIds, strategy })
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/conflicts/bulk-resolve", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ conflict_ids: conflictIds, strategy }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data: BulkResolveResponse = await response.json();
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success(`Resolved ${data.success} conflicts`);
|
||||
}
|
||||
refreshConflicts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to bulk resolve:', error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error('Failed to bulk resolve conflicts');
|
||||
}
|
||||
if (response.ok) {
|
||||
const data: BulkResolveResponse = await response.json();
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success(`Resolved ${data.success} conflicts`);
|
||||
}
|
||||
refreshConflicts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to bulk resolve:", error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error("Failed to bulk resolve conflicts");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function bulkDismiss(conflictIds: string[]): Promise<void> {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/conflicts/bulk-dismiss', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ conflict_ids: conflictIds })
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/conflicts/bulk-dismiss", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ conflict_ids: conflictIds }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success('Conflicts dismissed');
|
||||
}
|
||||
refreshConflicts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to dismiss conflicts:', error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error('Failed to dismiss conflicts');
|
||||
}
|
||||
if (response.ok) {
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success("Conflicts dismissed");
|
||||
}
|
||||
refreshConflicts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss conflicts:", error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error("Failed to dismiss conflicts");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function dismissAllResolved(): Promise<void> {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/conflicts/dismiss-resolved', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/conflicts/dismiss-resolved", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success('Resolved conflicts dismissed');
|
||||
}
|
||||
refreshConflicts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to dismiss resolved:', error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error('Failed to dismiss resolved conflicts');
|
||||
}
|
||||
if (response.ok) {
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success("Resolved conflicts dismissed");
|
||||
}
|
||||
refreshConflicts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss resolved:", error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error("Failed to dismiss resolved conflicts");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderConflicts(conflicts: ConflictDetailResponse[]): void {
|
||||
const container = document.getElementById('conflicts-list');
|
||||
if (!container) return;
|
||||
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;
|
||||
}
|
||||
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 => `
|
||||
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' ? `
|
||||
${
|
||||
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('');
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function updateConflictStats(data: ConflictListResponse): void {
|
||||
const totalEl = document.getElementById('conflicts-total');
|
||||
const unresolvedEl = document.getElementById('conflicts-unresolved');
|
||||
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);
|
||||
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('resolve-conflict-id') as HTMLInputElement;
|
||||
const modal = document.getElementById("resolve-modal");
|
||||
const conflictIdInput = document.getElementById(
|
||||
"resolve-conflict-id",
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (modal && conflictIdInput) {
|
||||
conflictIdInput.value = conflictId;
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
if (modal && conflictIdInput) {
|
||||
conflictIdInput.value = conflictId;
|
||||
modal.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function hideResolveModal(): void {
|
||||
const modal = document.getElementById('resolve-modal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
const modal = document.getElementById("resolve-modal");
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function handleResolveSubmit(event: Event): void {
|
||||
event.preventDefault();
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target as HTMLFormElement;
|
||||
const conflictId = (form.querySelector('#resolve-conflict-id') as HTMLInputElement)?.value;
|
||||
const winner = (form.querySelector('input[name="winner"]:checked') as HTMLInputElement)?.value;
|
||||
const form = event.target as HTMLFormElement;
|
||||
const conflictId = (
|
||||
form.querySelector("#resolve-conflict-id") as HTMLInputElement
|
||||
)?.value;
|
||||
const winner = (
|
||||
form.querySelector('input[name="winner"]:checked') as HTMLInputElement
|
||||
)?.value;
|
||||
|
||||
if (!conflictId || !winner) {
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error('Please select a winner');
|
||||
}
|
||||
return;
|
||||
if (!conflictId || !winner) {
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error("Please select a winner");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
resolveConflict(conflictId, winner);
|
||||
hideResolveModal();
|
||||
resolveConflict(conflictId, winner);
|
||||
hideResolveModal();
|
||||
}
|
||||
|
||||
(window as any).refreshConflicts = refreshConflicts;
|
||||
|
||||
Reference in New Issue
Block a user