113 lines
5.1 KiB
HTML
113 lines
5.1 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
<title>ComicOPDS — Library Dashboard</title>
|
|
<style>
|
|
:root { --gap: 16px; --card: #fff; --bg: #f6f7fb; --text:#222; --muted:#666; }
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif; background: var(--bg); color: var(--text); }
|
|
header { padding: 20px; background: #0f172a; color: #fff; }
|
|
header h1 { margin: 0 0 6px 0; font-size: 20px; }
|
|
header .sub { color: #cbd5e1; font-size: 12px; }
|
|
main { padding: 20px; max-width: 1200px; margin: 0 auto; }
|
|
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap); }
|
|
.card { background: var(--card); border-radius: 10px; padding: 16px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
|
|
.stat { display:flex; flex-direction:column; gap:6px; }
|
|
.stat .label { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
|
.stat .value { font-size: 28px; font-weight: 700; }
|
|
.charts { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--gap); margin-top: var(--gap); }
|
|
canvas { width: 100% !important; height: 360px !important; }
|
|
@media (max-width: 1000px) {
|
|
.grid { grid-template-columns: repeat(2, 1fr); }
|
|
.charts { grid-template-columns: 1fr; }
|
|
}
|
|
</style>
|
|
<!-- Chart.js from CDN -->
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1>ComicOPDS — Library Dashboard</h1>
|
|
<div class="sub">Last updated: <span id="lastUpdated">…</span> • Covers: <span id="covers">…</span></div>
|
|
</header>
|
|
|
|
<main>
|
|
<section class="grid" id="overview">
|
|
<div class="card stat"><div class="label">Total comics</div><div class="value" id="totalComics">0</div></div>
|
|
<div class="card stat"><div class="label">Unique series</div><div class="value" id="uniqueSeries">0</div></div>
|
|
<div class="card stat"><div class="label">Publishers</div><div class="value" id="uniquePublishers">0</div></div>
|
|
<div class="card stat"><div class="label">Formats</div><div class="value" id="formats">—</div></div>
|
|
</section>
|
|
|
|
<section class="charts">
|
|
<div class="card"><h3>Publishers distribution</h3><canvas id="publishersChart"></canvas></div>
|
|
<div class="card"><h3>Publication timeline</h3><canvas id="timelineChart"></canvas></div>
|
|
<div class="card"><h3>Formats breakdown</h3><canvas id="formatsChart"></canvas></div>
|
|
<div class="card"><h3>Top writers</h3><canvas id="writersChart"></canvas></div>
|
|
</section>
|
|
</main>
|
|
|
|
<script>
|
|
async function load() {
|
|
const r = await fetch("/stats.json", { credentials: "include" });
|
|
const data = await r.json();
|
|
|
|
// Stats
|
|
document.getElementById("lastUpdated").textContent = new Date(data.last_updated * 1000).toLocaleString();
|
|
document.getElementById("covers").textContent = data.total_covers;
|
|
document.getElementById("totalComics").textContent = data.total_comics;
|
|
document.getElementById("uniqueSeries").textContent = data.unique_series;
|
|
document.getElementById("uniquePublishers").textContent = data.unique_publishers;
|
|
document.getElementById("formats").textContent = Object.entries(data.formats)
|
|
.map(([k,v]) => `${k.toUpperCase()}: ${v}`).join(" ");
|
|
|
|
// Charts
|
|
const ctx1 = document.getElementById("publishersChart");
|
|
const pubLabels = data.publishers.labels;
|
|
const pubValues = data.publishers.values;
|
|
|
|
new Chart(ctx1, {
|
|
type: "doughnut",
|
|
data: { labels: pubLabels, datasets: [{ data: pubValues }] },
|
|
options: { responsive: true, plugins: { legend: { position: "bottom" } } }
|
|
});
|
|
|
|
const ctx2 = document.getElementById("timelineChart");
|
|
const years = data.timeline.labels;
|
|
const counts = data.timeline.values;
|
|
new Chart(ctx2, {
|
|
type: "line",
|
|
data: { labels: years, datasets: [{ label: "Issues per year", data: counts, tension: 0.2 }] },
|
|
options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { ticks: { autoSkip: true, maxTicksLimit: 12 } } } }
|
|
});
|
|
|
|
const ctx3 = document.getElementById("formatsChart");
|
|
const fmtLabels = Object.keys(data.formats);
|
|
const fmtValues = Object.values(data.formats);
|
|
new Chart(ctx3, {
|
|
type: "bar",
|
|
data: { labels: fmtLabels, datasets: [{ label: "Files", data: fmtValues }] },
|
|
options: { responsive: true, plugins: { legend: { display: false } } }
|
|
});
|
|
|
|
const ctx4 = document.getElementById("writersChart");
|
|
const wLabels = data.top_writers.labels;
|
|
const wValues = data.top_writers.values;
|
|
new Chart(ctx4, {
|
|
type: "bar",
|
|
data: { labels: wLabels, datasets: [{ label: "Issues", data: wValues }] },
|
|
options: {
|
|
indexAxis: "y",
|
|
responsive: true,
|
|
plugins: { legend: { display: false } },
|
|
scales: { x: { beginAtZero: true } }
|
|
}
|
|
});
|
|
}
|
|
load();
|
|
</script>
|
|
</body>
|
|
</html>
|