@@ -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+
104164async 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>` ;
0 commit comments