Skip to content

Commit e05312c

Browse files
committed
feat(analytics): enhance top links display in admin analytics
- Introduced new functions to resolve link information and build sections for displaying top clicked links. - Updated the admin analytics rendering to show top clicked links for both today and a specified range, improving data visibility. - Added state properties to manage the expanded view of top links sections, enhancing user interaction. - Refactored existing click tracking logic to utilize the new functions, streamlining the codebase.
1 parent 511cbe1 commit e05312c

2 files changed

Lines changed: 67 additions & 27 deletions

File tree

frontend/js/admin.js

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,66 @@ function _refocusSearch() {
101101
}
102102

103103
// ===================== ANALYTICS =====================
104+
function resolveLinkInfo(linkId) {
105+
let info = { label: "Unknown Link", courseName: "Unknown Course" };
106+
AppState.dbPrograms.forEach((p) =>
107+
p.years.forEach((y) =>
108+
y.sems.forEach((s) =>
109+
s.courses.forEach((c) =>
110+
c.links.forEach((l) => {
111+
if (l.id == linkId) info = { label: l.label, courseName: c.name };
112+
}),
113+
),
114+
),
115+
),
116+
);
117+
AppState.dbExtra.forEach((r) =>
118+
r.links.forEach((l) => {
119+
if (l.id == linkId) info = { label: l.label, courseName: r.title };
120+
}),
121+
);
122+
return info;
123+
}
124+
125+
function buildTopLinksSection(title, clickEvents, expandedKey) {
126+
const clickMap = {};
127+
clickEvents.forEach((c) => {
128+
if (c.link_id) clickMap[c.link_id] = (clickMap[c.link_id] || 0) + 1;
129+
});
130+
const sorted = Object.entries(clickMap).sort((a, b) => b[1] - a[1]);
131+
if (!sorted.length) {
132+
return `<div class="chart-wrap" style="margin-top:20px;">
133+
<div class="chart-title">${title}</div>
134+
<div style="color:var(--muted);margin-top:16px;font-size:0.9rem;">No click data.</div>
135+
</div>`;
136+
}
137+
138+
const expanded = AppState[expandedKey];
139+
const limit = expanded ? 10 : 5;
140+
const items = sorted
141+
.slice(0, limit)
142+
.map(([linkId, count]) => {
143+
const info = resolveLinkInfo(linkId);
144+
return `<li><strong>${count}</strong> clicks: ${esc(info.label)} <span style="color:var(--muted);font-size:0.8rem">(${esc(info.courseName)})</span></li>`;
145+
})
146+
.join("");
147+
148+
const toggleFn =
149+
expandedKey === "analyticsTopLinksTodayExpanded"
150+
? "AppState.analyticsTopLinksTodayExpanded=!AppState.analyticsTopLinksTodayExpanded"
151+
: "AppState.analyticsTopLinksExpanded=!AppState.analyticsTopLinksExpanded";
152+
const expandBtn =
153+
sorted.length > 5
154+
? `<button class="filter-btn" style="margin-top:12px;" onclick="${toggleFn};renderAdminAnalytics()">${expanded ? "Show top 5" : "Show top 10"}</button>`
155+
: "";
156+
157+
return `<div class="chart-wrap" style="margin-top:20px;">
158+
<div class="chart-title">${title}</div>
159+
<ul style="list-style:none;padding:0;margin-top:16px;">${items}</ul>
160+
${expandBtn}
161+
</div>`;
162+
}
163+
104164
async function renderAdminAnalytics() {
105165
document.getElementById("adminContent").innerHTML = getAdminAnalyticsSkeleton();
106166
try {
@@ -153,30 +213,10 @@ async function renderAdminAnalytics() {
153213
.map((r) => `<button class="filter-btn ${AppState.analyticsRange === r ? "active" : ""}" onclick="AppState.analyticsRange='${r}';renderAdminAnalytics()">${r} days</button>`)
154214
.join("");
155215

156-
// Calculate top clicked links
157216
const clicksInRange = clicks.filter((c) => new Date(c.clicked_at) >= cutoff);
158-
const clickMap = {};
159-
clicksInRange.forEach((c) => {
160-
if (c.link_id) clickMap[c.link_id] = (clickMap[c.link_id] || 0) + 1;
161-
});
162-
163-
const topLinks = Object.entries(clickMap)
164-
.sort((a, b) => b[1] - a[1])
165-
.slice(0, 10)
166-
.map(([linkId, count]) => {
167-
// Resolve link label and course
168-
let info = { label: "Unknown Link", courseName: "Unknown Course" };
169-
AppState.dbPrograms.forEach(p => p.years.forEach(y => y.sems.forEach(s => s.courses.forEach(c => c.links.forEach(l => {
170-
if (l.id == linkId) info = { label: l.label, courseName: c.name };
171-
})))));
172-
AppState.dbExtra.forEach(r => r.links.forEach(l => {
173-
if (l.id == linkId) info = { label: l.label, courseName: r.title };
174-
}));
175-
return `<li><strong>${count}</strong> clicks: ${esc(info.label)} <span style="color:var(--muted);font-size:0.8rem">(${esc(info.courseName)})</span></li>`;
176-
})
177-
.join("");
178-
179-
const topLinksHtml = topLinks ? `<ul style="list-style:none;padding:0;margin-top:16px;">${topLinks}</ul>` : `<div style="color:var(--muted);margin-top:16px;font-size:0.9rem;">No click data in range.</div>`;
217+
const clicksToday = clicks.filter((c) => c.clicked_at.slice(0, 10) === todayStr);
218+
const topLinksTodayHtml = buildTopLinksSection("🔥 Top Clicked Links (today)", clicksToday, "analyticsTopLinksTodayExpanded");
219+
const topLinksHtml = buildTopLinksSection("🔥 Top Clicked Links (in range)", clicksInRange, "analyticsTopLinksExpanded");
180220

181221
document.getElementById("adminContent").innerHTML = `
182222
<div class="stat-grid">
@@ -190,10 +230,8 @@ async function renderAdminAnalytics() {
190230
<div class="analytics-range">${rangeButtons}</div>
191231
<div class="bar-chart-scroll"><div class="bar-chart">${barsHtml}</div></div>
192232
</div>
193-
<div class="chart-wrap" style="margin-top:20px;">
194-
<div class="chart-title">🔥 Top Clicked Links (in range)</div>
195-
${topLinksHtml}
196-
</div>
233+
${topLinksTodayHtml}
234+
${topLinksHtml}
197235
<p style="font-size:.78rem;color:var(--muted);margin-top:8px;">Each visit counted once per browser session.</p>`;
198236
} catch (e) {
199237
document.getElementById("adminContent").innerHTML = `<div class="empty">⚠️ Could not load analytics: ${e.message}</div>`;

frontend/js/state.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const AppState = {
3232
currentSem: "all",
3333
dbPrograms: [],
3434
analyticsRange: "30",
35+
analyticsTopLinksExpanded: false,
36+
analyticsTopLinksTodayExpanded: false,
3537
dbExtra: [],
3638
courseById: new Map(),
3739
linkById: new Map(),

0 commit comments

Comments
 (0)