From 5b77eb613477a732064c0f09e79046a135e17a7b Mon Sep 17 00:00:00 2001 From: Varna Sri Raman Date: Thu, 9 Jul 2026 04:06:32 +0000 Subject: [PATCH] Add insights dashboard and reading-progress bar Feature parity with Dev Case Studies: - Insights dashboard on the index (toggle in the results tools): stat tiles plus by-topic / by-type / by-access bar charts read from data/stats.json, with clickable topic bars that filter the library. - Reading-progress bar on resource detail pages, driven by scroll. Frontend-only; reuses the existing generated stats.json (no build change). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_012msh1nNMxadJd417S8FjP2 --- css/style.css | 36 ++++++++++++++++++++++++ index.html | 6 ++++ js/app.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ js/resource.js | 18 ++++++++++++ resource.html | 2 ++ 5 files changed, 137 insertions(+) diff --git a/css/style.css b/css/style.css index 2a1bfd9..62ed782 100644 --- a/css/style.css +++ b/css/style.css @@ -1708,3 +1708,39 @@ mark { a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: 4px; } + +/* ---------- Reading progress bar (resource pages) ---------- */ +.reading-progress { + position: fixed; top: 0; left: 0; right: 0; height: 3px; + z-index: 200; background: transparent; opacity: 0; + transition: opacity 0.25s; pointer-events: none; +} +.reading-progress.visible { opacity: 1; } +.reading-progress > span { + display: block; height: 100%; width: 0%; + background: var(--brand-grad); box-shadow: 0 0 8px rgba(42,157,143,0.4); + transition: width 0.1s linear; +} + +/* ---------- Insights dashboard ---------- */ +.insights-panel { + display: none; background: var(--color-surface); + border: 1px solid var(--color-border); border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); padding: 24px; margin: 0 0 20px; +} +.insights-panel.open { display: block; } +.insight-loading { padding: 24px; text-align: center; color: var(--color-text-muted); font-size: 0.85rem; } +.insight-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 24px; } +.insight-tile { display: flex; flex-direction: column; gap: 2px; padding: 16px; border-radius: var(--radius-md); background: var(--color-bg); border: 1px solid var(--color-border-light); } +.insight-tile-value { font-family: var(--font-serif); font-size: 1.55rem; font-weight: 700; color: var(--color-accent); line-height: 1; letter-spacing: -0.02em; } +.insight-tile-label { font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-text-muted); } +.insight-charts { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 24px 32px; } +.insight-group-title { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-muted); margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid var(--color-border-light); } +.insight-bar { display: grid; grid-template-columns: 1fr 90px 34px; align-items: center; gap: 8px; padding: 3px 4px; border-radius: var(--radius-sm); } +.insight-bar.clickable { cursor: pointer; } +.insight-bar.clickable:hover { background: var(--color-accent-light); } +.insight-bar-label { font-size: 0.76rem; color: var(--color-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.insight-bar-track { height: 7px; background: var(--color-border-light); border-radius: 4px; overflow: hidden; } +.insight-bar-fill { display: block; height: 100%; border-radius: 4px; background: var(--brand-grad); transition: width 0.6s cubic-bezier(0.2,0.8,0.2,1); } +.insight-bar-value { font-size: 0.74rem; font-weight: 700; color: var(--color-text-secondary); text-align: right; font-variant-numeric: tabular-nums; } +.insight-note { margin-top: 20px; font-size: 0.73rem; color: var(--color-text-muted); font-style: italic; } diff --git a/index.html b/index.html index 5616cce..fc89e86 100644 --- a/index.html +++ b/index.html @@ -196,6 +196,10 @@

Development Discourses

Loading resources...

+
+
+
diff --git a/js/app.js b/js/app.js index b9b073a..717f6e8 100644 --- a/js/app.js +++ b/js/app.js @@ -98,6 +98,81 @@ applyFilters(); bindEvents(); + setupInsights(); + } + + // ---- Insights dashboard ---- + function setupInsights() { + const btn = document.getElementById('insightsBtn'); + const panel = document.getElementById('insightsPanel'); + if (!btn || !panel) return; + let loaded = false; + + function barChart(title, obj, clickTopic) { + const rows = Object.keys(obj || {}).map(k => ({ label: k, count: obj[k] })) + .sort((a, b) => b.count - a.count); + if (!rows.length) return ''; + const max = Math.max.apply(null, rows.map(r => r.count)); + const bars = rows.map(r => { + const pct = Math.round((r.count / max) * 100); + const attrs = clickTopic ? ` data-topic="${escapeHtml(r.label)}" role="button" tabindex="0"` : ''; + return `
+ ${escapeHtml(prettyLabel(r.label))} + + ${r.count} +
`; + }).join(''); + return `

${escapeHtml(title)}

${bars}
`; + } + + function prettyLabel(k) { + return ({ grey_literature: 'Grey literature', paper: 'Papers', book: 'Books', + open_access: 'Open access', free_to_read: 'Free to read', check_access: 'Check access' })[k] || k; + } + + function render(s) { + const tiles = [ + { v: s.total, l: 'Resources' }, + { v: s.topic_count, l: 'Topics' }, + { v: (s.by_type && s.by_type.paper) || 0, l: 'Papers' }, + { v: (s.by_access && s.by_access.open_access) || 0, l: 'Open access' }, + { v: (s.year_min && s.year_max) ? (s.year_min + '–' + s.year_max) : '—', l: 'Year span' }, + ]; + panel.innerHTML = + '
' + + tiles.map(x => `
${x.v}${x.l}
`).join('') + + '
' + + barChart('By topic', s.by_topic, true) + + barChart('By type', s.by_type, false) + + barChart('By access', s.by_access, false) + + '

Generated from the library index. Click a topic bar to filter.

'; + + panel.querySelectorAll('[data-topic]').forEach(el => { + const apply = () => { + currentTopic = el.dataset.topic; + setActivePill(topicFilters, currentTopic); + displayCount = PAGE_SIZE; + applyFilters(); + document.querySelector('.results-section').scrollIntoView({ behavior: 'smooth' }); + }; + el.addEventListener('click', apply); + el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); apply(); } }); + }); + } + + btn.addEventListener('click', function () { + const open = panel.classList.toggle('open'); + btn.classList.toggle('active', open); + btn.setAttribute('aria-expanded', String(open)); + if (open && !loaded) { + loaded = true; + panel.innerHTML = '
Loading insights…
'; + fetch('data/stats.json') + .then(r => r.ok ? r.json() : Promise.reject()) + .then(render) + .catch(() => { panel.innerHTML = '
Insights unavailable.
'; }); + } + }); } function showLoadingSkeleton() { diff --git a/js/resource.js b/js/resource.js index 11c78c4..467a085 100644 --- a/js/resource.js +++ b/js/resource.js @@ -48,6 +48,24 @@ window.renderLitmap(currentResource, allResources); } bindEvents(); + initReadingProgress(); + } + + // ---- Reading progress bar ---- + function initReadingProgress() { + const bar = document.getElementById('reading-progress'); + if (!bar) return; + const fill = bar.firstElementChild; + function update() { + const doc = document.documentElement; + const scrollable = doc.scrollHeight - doc.clientHeight; + const pct = scrollable > 0 ? Math.min(100, (window.scrollY / scrollable) * 100) : 0; + if (fill) fill.style.width = pct + '%'; + bar.classList.toggle('visible', window.scrollY > 60); + } + window.addEventListener('scroll', update, { passive: true }); + window.addEventListener('resize', update, { passive: true }); + update(); } // ---- Render Main ---- diff --git a/resource.html b/resource.html index e8f66c4..c835b89 100644 --- a/resource.html +++ b/resource.html @@ -37,6 +37,8 @@ + +