Files
bookhoard/web/src/analytics.ts
T
john-okeefe ea5ad7a41b 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.
2026-02-27 17:06:48 -05:00

127 lines
5.0 KiB
TypeScript

async function loadAnalytics(): Promise<void> {
const token = localStorage.getItem("token");
if (!token) return;
try {
const [statsRes, devicesRes, popularRes] = await Promise.all([
fetch("/api/analytics/stats", {
headers: { Authorization: `Bearer ${token}` },
}),
fetch("/api/analytics/devices", {
headers: { Authorization: `Bearer ${token}` },
}),
fetch("/api/analytics/popular", {
headers: { Authorization: `Bearer ${token}` },
}),
]);
if (statsRes.ok) {
const stats: ReadingStatsResponse = await statsRes.json();
renderReadingStats(stats);
}
if (devicesRes.ok) {
const devices: DeviceUsageResponse = await devicesRes.json();
renderDeviceUsage(devices);
}
if (popularRes.ok) {
const popular: PopularBooksResponse = await popularRes.json();
renderPopularBooks(popular);
}
} catch (error) {
console.error("Failed to load analytics:", error);
if ((window as any).showToast?.error) {
(window as any).showToast.error("Failed to load analytics data");
}
}
}
function renderReadingStats(stats: ReadingStatsResponse): void {
const container = document.getElementById("reading-stats");
if (!container) return;
container.innerHTML = `
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="stat-card p-4 rounded-lg" style="background-color: var(--bg-secondary)">
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_books_read}</p>
<p class="text-sm" style="color: var(--text-secondary)">Books Read</p>
</div>
<div class="stat-card p-4 rounded-lg" style="background-color: var(--bg-secondary)">
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_pages_read}</p>
<p class="text-sm" style="color: var(--text-secondary)">Pages Read</p>
</div>
<div class="stat-card p-4 rounded-lg" style="background-color: var(--bg-secondary)">
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_reading_time_minutes}</p>
<p class="text-sm" style="color: var(--text-secondary)">Minutes Reading</p>
</div>
<div class="stat-card p-4 rounded-lg" style="background-color: var(--bg-secondary)">
<p class="text-2xl font-bold" style="color: var(--text-primary)">${Math.round(stats.completion_rate * 100)}%</p>
<p class="text-sm" style="color: var(--text-secondary)">Completion Rate</p>
</div>
</div>
`;
}
function renderDeviceUsage(devices: DeviceUsageResponse): void {
const container = document.getElementById("device-usage");
if (!container) return;
if (!devices.devices || devices.devices.length === 0) {
container.innerHTML =
'<p style="color: var(--text-secondary)">No device usage data available</p>';
return;
}
container.innerHTML = devices.devices
.map(
(device) => `
<div class="p-3 rounded-lg border" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex justify-between items-center">
<div>
<p class="font-medium" style="color: var(--text-primary)">${device.device_name}</p>
<p class="text-sm" style="color: var(--text-secondary)">${device.device_type}</p>
</div>
<div class="text-right">
<p class="font-medium" style="color: var(--text-primary)">${Math.round(device.total_time_minutes)} min</p>
<p class="text-sm" style="color: var(--text-secondary)">${device.sync_count} syncs</p>
</div>
</div>
</div>
`,
)
.join("");
}
function renderPopularBooks(popular: PopularBooksResponse): void {
const container = document.getElementById("popular-books");
if (!container) return;
if (!popular.books || popular.books.length === 0) {
container.innerHTML =
'<p style="color: var(--text-secondary)">No reading history available</p>';
return;
}
container.innerHTML = popular.books
.map(
(book) => `
<div class="p-3 rounded-lg border flex items-center space-x-3" style="background-color: var(--bg-secondary); border-color: var(--border)">
<div class="flex-1">
<p class="font-medium" style="color: var(--text-primary)">${book.title}</p>
<p class="text-sm" style="color: var(--text-secondary)">${book.author}</p>
</div>
<div class="text-right">
<p class="font-medium" style="color: var(--text-primary)">${book.read_count}x</p>
<p class="text-sm" style="color: var(--text-secondary)">${Math.round(book.avg_completion * 100)}%</p>
</div>
</div>
`,
)
.join("");
}
document.addEventListener("DOMContentLoaded", loadAnalytics);
(window as any).loadAnalytics = loadAnalytics;