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:
+129
-100
@@ -1,31 +1,34 @@
|
||||
async function loadUnlinkedBooks(): Promise<void> {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sync/unlinked-books', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/sync/unlinked-books", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
renderUnlinkedBooks(data.unlinked || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load unlinked books:', error);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
renderUnlinkedBooks(data.unlinked || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load unlinked books:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderUnlinkedBooks(books: UnlinkedBookData[]): void {
|
||||
const container = document.getElementById('unlinked-books-list');
|
||||
if (!container) return;
|
||||
const container = document.getElementById("unlinked-books-list");
|
||||
if (!container) return;
|
||||
|
||||
if (books.length === 0) {
|
||||
container.innerHTML = '<p class="text-center p-4" style="color: var(--text-secondary)">No unlinked books</p>';
|
||||
return;
|
||||
}
|
||||
if (books.length === 0) {
|
||||
container.innerHTML =
|
||||
'<p class="text-center p-4" style="color: var(--text-secondary)">No unlinked books</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = books.map(book => `
|
||||
container.innerHTML = books
|
||||
.map(
|
||||
(book) => `
|
||||
<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>
|
||||
@@ -38,10 +41,15 @@ function renderUnlinkedBooks(books: UnlinkedBookData[]): void {
|
||||
<button onclick="window.showMatchModal('${book.progress_id}')" class="btn-primary px-3 py-1 rounded text-sm">Link</button>
|
||||
</div>
|
||||
</div>
|
||||
${book.potential_matches && book.potential_matches.length > 0 ? `
|
||||
${
|
||||
book.potential_matches && book.potential_matches.length > 0
|
||||
? `
|
||||
<div class="mt-3 pt-3 border-t" style="border-color: var(--border)">
|
||||
<p class="text-xs font-medium mb-2" style="color: var(--text-secondary)">Potential Matches:</p>
|
||||
${book.potential_matches.slice(0, 3).map(match => `
|
||||
${book.potential_matches
|
||||
.slice(0, 3)
|
||||
.map(
|
||||
(match) => `
|
||||
<div class="flex justify-between items-center p-2 rounded mb-1" style="background-color: var(--bg-primary)">
|
||||
<div>
|
||||
<p class="text-sm" style="color: var(--text-primary)">${match.title}</p>
|
||||
@@ -49,106 +57,125 @@ function renderUnlinkedBooks(books: UnlinkedBookData[]): void {
|
||||
</div>
|
||||
<button onclick="window.linkBook('${book.progress_id}', '${match.media_item_id}')" class="btn-secondary px-2 py-1 rounded text-xs">Link</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
` : ''}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`).join('');
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
async function linkBook(progressId: string, mediaItemId: string): Promise<void> {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
async function linkBook(
|
||||
progressId: string,
|
||||
mediaItemId: string,
|
||||
): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sync/link-book', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ progress_id: progressId, media_item_id: mediaItemId })
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/sync/link-book", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
progress_id: progressId,
|
||||
media_item_id: mediaItemId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success('Book linked successfully');
|
||||
}
|
||||
loadUnlinkedBooks();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error(error.error || 'Failed to link book');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to link book:', error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error('Failed to link book');
|
||||
}
|
||||
if (response.ok) {
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success("Book linked successfully");
|
||||
}
|
||||
loadUnlinkedBooks();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error(error.error || "Failed to link book");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to link book:", error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error("Failed to link book");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function autoLinkBooks(): Promise<void> {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
if (!confirm('Auto-link all books with high confidence matches?')) return;
|
||||
if (!confirm("Auto-link all books with high confidence matches?")) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sync/auto-link', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ confidence_threshold: 0.9 })
|
||||
});
|
||||
try {
|
||||
const response = await fetch("/api/sync/auto-link", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ confidence_threshold: 0.9 }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success(`Auto-linked ${data.linked_count || 0} books`);
|
||||
}
|
||||
loadUnlinkedBooks();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to auto-link:', error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error('Failed to auto-link books');
|
||||
}
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if ((window as any).showToast?.success) {
|
||||
(window as any).showToast.success(
|
||||
`Auto-linked ${data.linked_count || 0} books`,
|
||||
);
|
||||
}
|
||||
loadUnlinkedBooks();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to auto-link:", error);
|
||||
if ((window as any).showToast?.error) {
|
||||
(window as any).showToast.error("Failed to auto-link books");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getSuggestions(progressId: 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/sync/suggestions/${progressId}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`/api/sync/suggestions/${progressId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const suggestions = await response.json();
|
||||
showSuggestionsModal(progressId, suggestions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get suggestions:', error);
|
||||
if (response.ok) {
|
||||
const suggestions = await response.json();
|
||||
showSuggestionsModal(progressId, suggestions);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get suggestions:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function showSuggestionsModal(progressId: string, suggestions: PotentialMatchData[]): void {
|
||||
const modal = document.getElementById('match-modal');
|
||||
const content = document.getElementById('match-modal-content');
|
||||
function showSuggestionsModal(
|
||||
progressId: string,
|
||||
suggestions: PotentialMatchData[],
|
||||
): void {
|
||||
const modal = document.getElementById("match-modal");
|
||||
const content = document.getElementById("match-modal-content");
|
||||
|
||||
if (!modal || !content) return;
|
||||
if (!modal || !content) return;
|
||||
|
||||
content.innerHTML = `
|
||||
content.innerHTML = `
|
||||
<div class="p-4">
|
||||
<h3 class="font-medium mb-4" style="color: var(--text-primary)">Select a match</h3>
|
||||
<div class="space-y-2">
|
||||
${suggestions.map(s => `
|
||||
${suggestions
|
||||
.map(
|
||||
(s) => `
|
||||
<div class="p-3 rounded border cursor-pointer hover:border-opacity-50"
|
||||
style="background-color: var(--bg-primary); border-color: var(--border)"
|
||||
onclick="window.linkBook('${progressId}', '${s.media_item_id}'); window.hideMatchModal();">
|
||||
@@ -156,20 +183,22 @@ function showSuggestionsModal(progressId: string, suggestions: PotentialMatchDat
|
||||
<p class="text-sm" style="color: var(--text-secondary)">${s.author}</p>
|
||||
<p class="text-xs" style="color: var(--text-secondary)">${Math.round(s.confidence * 100)}% match</p>
|
||||
</div>
|
||||
`).join('')}
|
||||
`,
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
<button onclick="window.hideMatchModal()" class="mt-4 btn-secondary w-full py-2 rounded">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.remove("hidden");
|
||||
}
|
||||
|
||||
function hideMatchModal(): void {
|
||||
const modal = document.getElementById('match-modal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
const modal = document.getElementById("match-modal");
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).loadUnlinkedBooks = loadUnlinkedBooks;
|
||||
|
||||
Reference in New Issue
Block a user