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.
127 lines
5.0 KiB
TypeScript
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;
|