- Remove DOMContentLoaded event listeners from analytics.ts and docs.ts - Rely on x-init attribute in templates for page initialization - Clean up unused exports from collections.ts Alpine data - Add x-init calls to admin_library, analytics, and docs templates - Normalize quote style in collections WebSocket script (single to double) - Disable Add Books button in collection detail (pending implementation)
130 lines
4.9 KiB
TypeScript
130 lines
4.9 KiB
TypeScript
import { showToast } from "./toast";
|
|
import { Alpine } from "./alpine";
|
|
|
|
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);
|
|
showToast("Failed to load analytics data", "error");
|
|
}
|
|
}
|
|
|
|
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("");
|
|
}
|
|
|
|
export { loadAnalytics };
|
|
|
|
Alpine.data("analytics", () => ({
|
|
loadAnalytics,
|
|
}));
|