- Add search.ts - header search with keyboard navigation - Debounced search with 300ms delay - Arrow key navigation through results - Escape to close, Enter to select - Library type icons and highlighting - Add collections.ts - collection and rule management - Rule CRUD operations (create, update, delete) - Rule testing functionality - Bulk collection operations - Add bookshelf.ts - book display and navigation - Library selection state management - Book viewing interactions - Pagination logic - Add linking.ts - book matching and manual linking - Search and match functionality - Manual link modal - Bulk auto-link and suggestions - Add api-explorer.ts - API testing interface - Request/response display - cURL command generation - History tracking - Add admin.ts - admin dashboard actions - Library scan triggers - System statistics display - Profile management - Add analytics.ts - analytics data loading - Chart.js integration - Daily reading minutes chart - Device usage and popular books display - Add queue.ts - sync queue management - Process pending items - Clear failed/all items - Filter by status, type, device - Add conflicts.ts - conflict resolution - Individual and bulk resolve operations - Winner device selection - Manual override inputs - Add docs.ts - documentation search - Lunr.js search integration - Sidebar toggle for mobile
188 lines
6.8 KiB
TypeScript
188 lines
6.8 KiB
TypeScript
import type { CollectionData, CollectionRule } from './types/api';
|
|
|
|
async function loadCollections(): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/collections', {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
renderCollections(data.collections || []);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load collections:', error);
|
|
}
|
|
}
|
|
|
|
function renderCollections(collections: CollectionData[]): void {
|
|
const container = document.getElementById('collections-list');
|
|
if (!container) return;
|
|
|
|
if (collections.length === 0) {
|
|
container.innerHTML = '<p class="text-center p-4" style="color: var(--text-secondary)">No collections yet</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = collections.map(collection => `
|
|
<a href="/collections/${collection.id}" class="block p-4 rounded-lg border transition-colors hover:border-opacity-50"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border)">
|
|
<div class="flex items-center space-x-3">
|
|
<span class="text-2xl">${collection.icon || '📁'}</span>
|
|
<div>
|
|
<h3 class="font-medium" style="color: var(--text-primary)">${collection.name}</h3>
|
|
${collection.description ? `<p class="text-sm" style="color: var(--text-secondary)">${collection.description}</p>` : ''}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
`).join('');
|
|
}
|
|
|
|
async function loadCollectionRules(collectionId: string): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/collections/${collectionId}/rules`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
const rules: CollectionRule[] = await response.json();
|
|
renderRules(rules);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load rules:', error);
|
|
}
|
|
}
|
|
|
|
function renderRules(rules: CollectionRule[]): void {
|
|
const container = document.getElementById('rules-list');
|
|
if (!container) return;
|
|
|
|
if (rules.length === 0) {
|
|
container.innerHTML = '<p class="text-center p-4" style="color: var(--text-secondary)">No rules defined</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = rules.map(rule => `
|
|
<div class="p-3 rounded-lg border mb-2 flex justify-between items-center"
|
|
style="background-color: var(--bg-secondary); border-color: var(--border)">
|
|
<div>
|
|
<p class="font-medium" style="color: var(--text-primary)">${rule.field} ${rule.operator} "${rule.value}"</p>
|
|
<p class="text-sm" style="color: var(--text-secondary)">Priority: ${rule.priority} | ${rule.enabled ? 'Enabled' : 'Disabled'}</p>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<button onclick="window.editRule('${rule.id}')" class="btn-secondary px-2 py-1 rounded text-sm">Edit</button>
|
|
<button onclick="window.deleteRule('${rule.id}')" class="btn-secondary px-2 py-1 rounded text-sm">Delete</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
async function createRule(collectionId: string, rule: Partial<CollectionRule>): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/collections/${collectionId}/rules`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(rule)
|
|
});
|
|
|
|
if (response.ok) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success('Rule created');
|
|
}
|
|
loadCollectionRules(collectionId);
|
|
} else {
|
|
const error = await response.json();
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error(error.error || 'Failed to create rule');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to create rule:', error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error('Failed to create rule');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function deleteRule(collectionId: string, ruleId: string): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
if (!confirm('Are you sure you want to delete this rule?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/collections/${collectionId}/rules/${ruleId}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
if ((window as any).showToast?.success) {
|
|
(window as any).showToast.success('Rule deleted');
|
|
}
|
|
loadCollectionRules(collectionId);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to delete rule:', error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error('Failed to delete rule');
|
|
}
|
|
}
|
|
}
|
|
|
|
async function testRule(collectionId: string, rule: Partial<CollectionRule>): Promise<void> {
|
|
const token = localStorage.getItem('token');
|
|
if (!token) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/collections/${collectionId}/rules/test`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(rule)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const results = await response.json();
|
|
renderTestResults(results);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to test rule:', error);
|
|
if ((window as any).showToast?.error) {
|
|
(window as any).showToast.error('Failed to test rule');
|
|
}
|
|
}
|
|
}
|
|
|
|
function renderTestResults(results: unknown[]): void {
|
|
const container = document.getElementById('test-results');
|
|
if (!container) return;
|
|
|
|
if (!results || (Array.isArray(results) && results.length === 0)) {
|
|
container.innerHTML = '<p class="p-2 text-sm" style="color: var(--text-secondary)">No matching books found</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = `<p class="p-2 text-sm" style="color: var(--text-secondary)">${Array.isArray(results) ? results.length : 0} matching books</p>`;
|
|
}
|
|
|
|
(window as any).loadCollections = loadCollections;
|
|
(window as any).loadCollectionRules = loadCollectionRules;
|
|
(window as any).createRule = createRule;
|
|
(window as any).deleteRule = deleteRule;
|
|
(window as any).testRule = testRule;
|