Features: - Complete dashboard redesign with improved UI components and layout - Implement custom section builder for personalized book organization - Add new events tracking system for user interactions - Enhance search functionality with better static search.js - Update TypeScript type definitions for API responses Backend: - Update Go dependencies in go.mod - Add new frontend routes in router Templates: - Update admin and dashboard templates with new components Frontend: - Refactor analytics, collections, conflicts, and queue modules - Add new documentation features in docs.ts - Implement linking between books and collections - Add toast notifications for user feedback - Include placeholder book SVG asset This commit consolidates multiple feature additions and improvements across the entire stack including backend, templates, and frontend.
186 lines
6.7 KiB
TypeScript
186 lines
6.7 KiB
TypeScript
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;
|