11document . addEventListener ( "DOMContentLoaded" , ( ) => {
2+ initPaperFadeInDelays ( ) ;
3+ initCitationsAndScholarWidget ( ) ;
4+ initContactViewToggle ( ) ;
5+ } ) ;
6+
7+ function initPaperFadeInDelays ( ) {
28 const papers = document . querySelectorAll ( ".paper-item" ) ;
39 papers . forEach ( ( paper , index ) => {
410 paper . style . animationDelay = `${ index * 0.25 } s` ;
511 } ) ;
6-
7- const citationElements = document . querySelectorAll ( '.citation[data-doi]' ) ;
8- const totalDisplay = document . querySelector ( '.citations' ) ;
9-
12+ }
13+
14+ function initCitationsAndScholarWidget ( ) {
15+ const citationElements = document . querySelectorAll ( ".citation[data-doi]" ) ;
16+ const totalDisplay = document . querySelector ( ".citations" ) ;
17+ const chart = document . getElementById ( "scholar-chart" ) ;
18+ const yLabel = document . getElementById ( "scholar-y-label" ) ;
19+
20+ // If none of the related elements exist, there's nothing to do.
21+ if ( citationElements . length === 0 && ! totalDisplay && ! chart ) {
22+ return ;
23+ }
24+
1025 const CITATIONS_URL = "/assets/data/citations.json" ;
11-
12- async function run ( ) {
26+
27+ async function loadAndRender ( ) {
1328 let data ;
1429 try {
1530 const res = await fetch ( CITATIONS_URL ) ;
@@ -19,131 +34,131 @@ document.addEventListener("DOMContentLoaded", () => {
1934 console . warn ( "Could not load citations.json:" , err ) ;
2035 return ;
2136 }
22-
23- const citationCounts = [ ] ;
24-
37+
38+ // Per-paper citations
2539 citationElements . forEach ( ( el ) => {
2640 const doi = el . getAttribute ( "data-doi" ) ;
2741 const raw = doi && Object . prototype . hasOwnProperty . call ( data , doi ) ? data [ doi ] : undefined ;
2842 const count = Number ( raw ) ;
2943
3044 if ( doi && Number . isFinite ( count ) ) {
31- citationCounts . push ( count ) ;
3245 el . textContent = `‒ CITATIONS: ${ count } ‒` ;
3346 } else {
3447 el . textContent = "" ;
3548 }
3649 } ) ;
37-
38- // Update total + h-index
50+
51+ // Footer summary
3952 if ( totalDisplay ) {
4053 const totalFromJson = Number ( data . _total_citations ) ;
4154 const hIndexFromJson = Number ( data . _h_index ) ;
42- totalDisplay . textContent = `‒ CITATIONS: ${ totalFromJson } · H-INDEX: ${ hIndexFromJson } ‒` ;
55+ if ( Number . isFinite ( totalFromJson ) && Number . isFinite ( hIndexFromJson ) ) {
56+ totalDisplay . textContent = `‒ CITATIONS: ${ totalFromJson } · H-INDEX: ${ hIndexFromJson } ‒` ;
57+ }
4358 }
4459
60+ // Scholar mini chart
4561 const yearsData = data . _yearly_totals ;
46- if ( ! yearsData ) return ;
62+ if ( ! yearsData || typeof yearsData !== "object" ) return ;
63+ if ( ! chart || ! yLabel ) return ;
4764
48- const chart = document . getElementById ( 'scholar-chart' ) ;
49-
50- // 1. Grab the last 4 years (adjust .slice(-4) if you want more/fewer bars)
5165 const allYears = Object . keys ( yearsData ) . sort ( ) ;
52- const displayYears = allYears . slice ( - 4 ) ;
53-
54- if ( displayYears . length > 0 ) {
55- // 2. Find max citations for the Y-Axis label and bar scaling
56- const maxCitations = Math . max ( ...displayYears . map ( y => yearsData [ y ] ) ) ;
57- document . getElementById ( 'scholar-y-label' ) . innerText = maxCitations ;
58-
59- // 3. Render the bars and the years under them
60- displayYears . forEach ( ( year , index ) => {
61- const count = yearsData [ year ] ;
62-
63- // Column container
64- const col = document . createElement ( 'div' ) ;
65- col . className = 'scholar-col' ;
66-
67- // The visual bar
68- const bar = document . createElement ( ' div' ) ;
69- bar . className = ' scholar-bar' ;
70- bar . title = `${ year } : ${ count } citations` ;
71-
72- setTimeout ( ( ) => {
73- bar . style . height = maxCitations > 0 ? ` ${ ( count / maxCitations ) * 100 } %` : '0%' ;
74- } , index * 300 ) ;
75-
76- const yearLabel = document . createElement ( 'div' ) ;
77- yearLabel . className = 'scholar-year' ;
78- yearLabel . innerText = year ;
79-
80- col . appendChild ( bar ) ;
81- col . appendChild ( yearLabel ) ;
82- chart . appendChild ( col ) ;
83- } ) ;
84- }
66+ const displayYears = allYears . slice ( - 4 ) ;
67+ if ( displayYears . length === 0 ) return ;
68+
69+ const values = displayYears . map ( ( y ) => Number ( yearsData [ y ] ) || 0 ) ;
70+ const maxCitations = Math . max ( ...values ) ;
71+ yLabel . textContent = String ( maxCitations ) ;
72+
73+ // Ensure we don't duplicate bars if this ever runs twice.
74+ chart . innerHTML = "" ;
75+
76+ displayYears . forEach ( ( year , index ) => {
77+ const count = Number ( yearsData [ year ] ) || 0 ;
78+
79+ const col = document . createElement ( "div" ) ;
80+ col . className = "scholar-col" ;
81+
82+ const bar = document . createElement ( " div" ) ;
83+ bar . className = " scholar-bar" ;
84+ bar . title = `${ year } : ${ count } citations` ;
85+
86+ setTimeout ( ( ) => {
87+ const height = maxCitations > 0 ? ( count / maxCitations ) * 100 : 0 ;
88+ bar . style . height = ` ${ height } %` ;
89+ } , index * 300 ) ;
90+
91+ const yearLabel = document . createElement ( "div" ) ;
92+ yearLabel . className = "scholar- year" ;
93+ yearLabel . innerText = year ;
94+
95+ col . appendChild ( bar ) ;
96+ col . appendChild ( yearLabel ) ;
97+ chart . appendChild ( col ) ;
98+ } ) ;
8599 }
86-
100+
87101 window . addEventListener ( "load" , ( ) => {
88102 // Use idle time if available
89103 if ( "requestIdleCallback" in window ) {
90104 requestIdleCallback ( ( ) => {
91- run ( ) ;
105+ loadAndRender ( ) ;
92106 } ) ;
93107 } else {
94108 // Fallback
95- setTimeout ( run , 200 ) ;
109+ setTimeout ( loadAndRender , 200 ) ;
96110 }
97111 } ) ;
112+ }
98113
114+ function initContactViewToggle ( ) {
99115 const toggleText = {
100- default : ' CONTACT' ,
101- alternate : ' HOME'
116+ default : " CONTACT" ,
117+ alternate : " HOME" ,
102118 } ;
103119
104- const CONTACT_HASH = ' #contact' ;
120+ const CONTACT_HASH = " #contact" ;
105121
106122 // --- Selectors ---
107- const toggleBtn = document . getElementById ( ' contact-toggle' ) || document . querySelector ( 'a[href="#contact"]' ) ;
108- const mainBlockquote = document . querySelector ( ' blockquote' ) ;
109- const paperItems = document . querySelectorAll ( ' .paper-item' ) ;
110- const footer = document . querySelector ( ' footer' ) ;
123+ const toggleBtn = document . getElementById ( " contact-toggle" ) || document . querySelector ( 'a[href="#contact"]' ) ;
124+ const mainBlockquote = document . querySelector ( " blockquote" ) ;
125+ const paperItems = Array . from ( document . querySelectorAll ( " .paper-item" ) ) ;
126+ const footer = document . querySelector ( " footer" ) ;
111127
112128 // If there's no CONTACT link (or markup changed), skip the contact-view feature.
113129 if ( ! toggleBtn ) {
114130 return ;
115131 }
116132
117133 function escapeHtml ( str ) {
118- const div = document . createElement ( ' div' ) ;
134+ const div = document . createElement ( " div" ) ;
119135 div . textContent = str ;
120136 return div . innerHTML ;
121137 }
122138
123139 function asStringArray ( value ) {
124140 if ( ! Array . isArray ( value ) ) return [ ] ;
125- return value . filter ( v => typeof v === ' string' ) ;
141+ return value . filter ( ( v ) => typeof v === " string" ) ;
126142 }
127143
128144 function getOrCreateEmailBlockContainer ( ) {
129- let el = document . getElementById ( ' email-blockquote' ) ;
145+ let el = document . getElementById ( " email-blockquote" ) ;
130146 if ( el ) return el ;
131147
132- el = document . createElement ( ' section' ) ;
133- el . id = ' email-blockquote' ;
134- el . className = ' email-blockquote hidden-view' ;
135- el . setAttribute ( ' aria-label' , ' Contact' ) ;
136- el . setAttribute ( ' data-nosnippet' , '' ) ;
148+ el = document . createElement ( " section" ) ;
149+ el . id = " email-blockquote" ;
150+ el . className = " email-blockquote hidden-view" ;
151+ el . setAttribute ( " aria-label" , " Contact" ) ;
152+ el . setAttribute ( " data-nosnippet" , "" ) ;
137153
138- const insertionPoint = mainBlockquote || document . querySelector ( ' .col-xs-12' ) ;
154+ const insertionPoint = mainBlockquote || document . querySelector ( " .col-xs-12" ) ;
139155 if ( insertionPoint ) {
140- insertionPoint . insertAdjacentElement ( ' afterend' , el ) ;
156+ insertionPoint . insertAdjacentElement ( " afterend" , el ) ;
141157 }
142158
143159 return el ;
144160 }
145161
146- // --- Build email block from data ---
147162 function renderEmailBlock ( container , { active, inactive, error = false } ) {
148163 const activeEmails = asStringArray ( active ) ;
149164 const inactiveEmails = asStringArray ( inactive ) ;
@@ -153,120 +168,107 @@ document.addEventListener("DOMContentLoaded", () => {
153168 <div class="col-xs-12 col-sm-6">
154169 <span class="email-badge email-badge-active">Active</span>
155170 <ul class="list-unstyled email-list">
156- ${ activeEmails . map ( email => `
171+ ${ activeEmails
172+ . map (
173+ ( email ) => `
157174 <li class="email-item">
158175 <small>${ escapeHtml ( email ) } </small>
159176 </li>
160- ` ) . join ( '' ) }
177+ `
178+ )
179+ . join ( "" ) }
161180 </ul>
162181 </div>
163182 <div class="col-xs-12 col-sm-6">
164183 <span class="email-badge email-badge-inactive">Inactive</span>
165184 <ul class="list-unstyled email-list">
166- ${ inactiveEmails . map ( email => `
185+ ${ inactiveEmails
186+ . map (
187+ ( email ) => `
167188 <li class="email-item email-item-inactive">
168189 <small>${ escapeHtml ( email ) } </small>
169190 </li>
170- ` ) . join ( '' ) }
191+ `
192+ )
193+ . join ( "" ) }
171194 </ul>
172195 </div>
173196 </div>
174197 <p class="text-muted email-footer-note">
175- <small>${ error ? ' Error loading email addresses!' : ' Feel free to reach out via the active addresses.' } </small>
198+ <small>${ error ? " Error loading email addresses!" : " Feel free to reach out via the active addresses." } </small>
176199 </p>
177200 ` ;
178201 }
179202
180- // --- Collect elements to toggle ---
181- const elementsToToggle = [
182- mainBlockquote ,
183- ...paperItems ,
184- footer
185- ] . filter ( el => el !== null ) ;
186-
187- // --- State variables ---
188- let showingEmails = false ;
203+ const elementsToToggle = [ mainBlockquote , ...paperItems , footer ] . filter ( Boolean ) ;
189204 const emailBlockquote = getOrCreateEmailBlockContainer ( ) ;
190- let emailBlockRendered = false ;
191- let emailDataPromise = null ; // cache the fetch promise
205+
206+ let emailDataPromise = null ; // cache the fetch promise
207+ let viewSyncId = 0 ;
192208
193209 function isContactViewFromHash ( ) {
194210 return window . location . hash . toLowerCase ( ) === CONTACT_HASH ;
195211 }
196212
197213 function setHidden ( el , hidden ) {
198214 if ( ! el ) return ;
199- el . classList . toggle ( ' hidden-view' , hidden ) ;
215+ el . classList . toggle ( " hidden-view" , hidden ) ;
200216 }
201217
202218 function startEmailFetchIfNeeded ( ) {
203219 if ( emailDataPromise ) return ;
204220
205- toggleBtn . textContent = ' ...' ;
221+ toggleBtn . textContent = " ..." ;
206222
207- const timeout = new Promise ( ( _ , reject ) =>
208- setTimeout ( ( ) => reject ( new Error ( 'Request timed out' ) ) , 3000 )
209- ) ;
223+ const timeout = new Promise ( ( _ , reject ) => setTimeout ( ( ) => reject ( new Error ( "Request timed out" ) ) , 3000 ) ) ;
210224
211225 emailDataPromise = Promise . race ( [
212- fetch ( ' /assets/data/emails.json' ) . then ( response => {
213- if ( ! response . ok ) throw new Error ( ' Failed to load email data' ) ;
226+ fetch ( " /assets/data/emails.json" ) . then ( ( response ) => {
227+ if ( ! response . ok ) throw new Error ( " Failed to load email data" ) ;
214228 return response . json ( ) ;
215229 } ) ,
216- timeout
217- ] ) . catch ( error => {
218- console . error ( ' Could not load emails:' , error ) ;
230+ timeout ,
231+ ] ) . catch ( ( error ) => {
232+ console . error ( " Could not load emails:" , error ) ;
219233 emailDataPromise = null ; // allow retry next time
220234 return { active : [ ] , inactive : [ ] , error : true } ;
221235 } ) ;
222236 }
223237
224- let viewSyncId = 0 ;
225-
226238 async function syncContactViewFromHash ( ) {
227239 const syncId = ++ viewSyncId ;
228240 const shouldShowEmails = isContactViewFromHash ( ) ;
229241
230242 if ( shouldShowEmails ) {
231243 startEmailFetchIfNeeded ( ) ;
232244 const emailData = await emailDataPromise ;
233- if ( ! emailBlockRendered ) {
234- renderEmailBlock ( emailBlockquote , emailData ) ;
235- emailBlockRendered = true ;
236- }
245+
246+ // Always (re)render when data resolves so a retry after a failure can update the view.
247+ renderEmailBlock ( emailBlockquote , emailData ) ;
237248 }
238249
239250 // If another hash change happened while we were awaiting, ignore this pass.
240251 if ( syncId !== viewSyncId ) return ;
241252
242253 // Apply visibility deterministically (avoid toggle drift)
243- elementsToToggle . forEach ( el => setHidden ( el , shouldShowEmails ) ) ;
254+ elementsToToggle . forEach ( ( el ) => setHidden ( el , shouldShowEmails ) ) ;
244255 setHidden ( emailBlockquote , ! shouldShowEmails ) ;
245256
246257 toggleBtn . textContent = shouldShowEmails ? toggleText . alternate : toggleText . default ;
247- toggleBtn . setAttribute ( 'aria-expanded' , shouldShowEmails ? 'true' : 'false' ) ;
248- showingEmails = shouldShowEmails ;
258+ toggleBtn . setAttribute ( "aria-expanded" , shouldShowEmails ? "true" : "false" ) ;
249259 }
250260
251- // --- Click handler: drives state via URL hash ---
252261 function onToggleClick ( event ) {
253262 event . preventDefault ( ) ;
254-
255- if ( isContactViewFromHash ( ) ) {
256- window . location . hash = '' ;
257- } else {
258- window . location . hash = CONTACT_HASH ;
259- }
263+ window . location . hash = isContactViewFromHash ( ) ? "" : CONTACT_HASH ;
260264 }
261265
262- // Keep the view in sync with back/forward and direct links.
263- window . addEventListener ( 'hashchange' , ( ) => {
266+ window . addEventListener ( "hashchange" , ( ) => {
264267 syncContactViewFromHash ( ) ;
265268 } ) ;
266269
267270 // Initial sync (supports landing on /#contact)
268271 syncContactViewFromHash ( ) ;
269272
270- // --- Attach event listener ---
271- toggleBtn . addEventListener ( 'click' , onToggleClick ) ;
272- } ) ;
273+ toggleBtn . addEventListener ( "click" , onToggleClick ) ;
274+ }
0 commit comments