1+ document . addEventListener ( "DOMContentLoaded" , ( ) => {
2+ const papers = document . querySelectorAll ( ".paper-item" ) ;
3+ papers . forEach ( ( paper , index ) => {
4+ paper . style . animationDelay = `${ index * 0.25 } s` ;
5+ } ) ;
6+
7+ const citationElements = document . querySelectorAll ( '.citation[data-doi]' ) ;
8+ const totalDisplay = document . querySelector ( '.citations' ) ;
9+
10+ const CACHE_KEY = 'citation_cache' ;
11+ const CACHE_TTL = 7 * 24 * 60 * 60 * 1000 ;
12+
13+ function loadCache ( ) {
14+ try {
15+ const raw = localStorage . getItem ( CACHE_KEY ) ;
16+ if ( ! raw ) return null ;
17+ const { timestamp, data } = JSON . parse ( raw ) ;
18+ if ( Date . now ( ) - timestamp > CACHE_TTL ) {
19+ localStorage . removeItem ( CACHE_KEY ) ;
20+ return null ;
21+ }
22+ return data ; // { [doi]: count }
23+ } catch {
24+ return null ;
25+ }
26+ }
27+
28+ function saveCache ( data ) {
29+ try {
30+ localStorage . setItem ( CACHE_KEY , JSON . stringify ( { timestamp : Date . now ( ) , data } ) ) ;
31+ } catch {
32+ // localStorage full or unavailable — fail silently
33+ }
34+ }
35+
36+ async function fetchCount ( doi ) {
37+ const url = `https://api.crossref.org/works/${ encodeURIComponent ( doi ) } ` ;
38+ const res = await fetch ( url ) ;
39+ if ( ! res . ok ) throw new Error ( `CrossRef error for ${ doi } ` ) ;
40+ const json = await res . json ( ) ;
41+ return json . message [ 'is-referenced-by-count' ] || 0 ;
42+ }
43+
44+ async function run ( ) {
45+ const cachedData = loadCache ( ) ;
46+ const freshData = cachedData || { } ;
47+ let anyFailed = false ;
48+ const citationCounts = [ ] ;
49+
50+ await Promise . all ( [ ...citationElements ] . map ( async ( el ) => {
51+ const doi = el . getAttribute ( 'data-doi' ) ;
52+
53+ try {
54+ // Use cached count if available, otherwise fetch
55+ const count = ( doi in freshData ) ? freshData [ doi ] : await fetchCount ( doi ) ;
56+ freshData [ doi ] = count ;
57+ citationCounts . push ( count ) ;
58+ el . textContent = `‒ CITATIONS: ${ count } ‒` ;
59+ } catch {
60+ el . textContent = '' ;
61+ anyFailed = true ;
62+ }
63+ } ) ) ;
64+
65+ // Save whatever we successfully got
66+ if ( Object . keys ( freshData ) . length ) saveCache ( freshData ) ;
67+
68+ // Update total + h-index
69+ if ( totalDisplay && ! anyFailed ) {
70+ const sorted = citationCounts . slice ( ) . sort ( ( a , b ) => b - a ) ;
71+ let hIndex = 0 ;
72+ for ( let i = 0 ; i < sorted . length ; i ++ ) {
73+ if ( sorted [ i ] >= i + 1 ) hIndex = i + 1 ;
74+ else break ;
75+ }
76+ const total = citationCounts . reduce ( ( a , b ) => a + b , 0 ) ;
77+ totalDisplay . textContent = `‒ CITATIONS: ${ total } · H-INDEX: ${ hIndex } ‒` ;
78+ }
79+ }
80+
81+ run ( ) ;
82+
83+ const toggleText = {
84+ default : 'CONTACT' ,
85+ alternate : 'HOME'
86+ } ;
87+
88+ // --- Selectors ---
89+ const toggleBtn = document . querySelector ( 'sub a[href="#"]' ) ;
90+ const mainBlockquote = document . querySelector ( 'blockquote' ) ;
91+ const paperItems = document . querySelectorAll ( '.paper-item' ) ;
92+ const footer = document . querySelector ( 'footer' ) ;
93+
94+ // --- Inject CSS class for hiding elements ---
95+ const style = document . createElement ( 'style' ) ;
96+ style . textContent = `.hidden-view { display: none !important; }` ;
97+ document . head . appendChild ( style ) ;
98+
99+ function escapeHtml ( str ) {
100+ const div = document . createElement ( 'div' ) ;
101+ div . textContent = str ;
102+ return div . innerHTML ;
103+ }
104+
105+ // --- Build email block from data ---
106+ function createEmailBlockquote ( { active, inactive, error = false } ) {
107+ const container = document . createElement ( 'div' ) ;
108+ container . id = 'email-blockquote' ;
109+ container . style . marginTop = '1.5rem' ;
110+ container . setAttribute ( 'data-nosnippet' , '' ) ; // ← prevents Google from indexing this content
111+ container . classList . add ( 'hidden-view' ) ; // start hidden
112+
113+ container . innerHTML = `
114+ <div class="row">
115+ <div class="col-xs-12 col-sm-6">
116+ <span class="email-badge email-badge-active">Active</span>
117+ <ul class="list-unstyled" style="margin-top: 12px;">
118+ ${ active . map ( email => `
119+ <li style="padding: 5px 0; border-bottom: 1px solid #eee;">
120+ <small>${ escapeHtml ( email ) } </small>
121+ </li>
122+ ` ) . join ( '' ) }
123+ </ul>
124+ </div>
125+ <div class="col-xs-12 col-sm-6">
126+ <span class="email-badge email-badge-inactive">Inactive</span>
127+ <ul class="list-unstyled" style="margin-top: 12px;">
128+ ${ inactive . map ( email => `
129+ <li style="padding: 5px 0; border-bottom: 1px solid #eee; color: #777;">
130+ <small>${ escapeHtml ( email ) } </small>
131+ </li>
132+ ` ) . join ( '' ) }
133+ </ul>
134+ </div>
135+ </div>
136+ <p class="text-muted" style="margin-top: 20px; font-style: italic;">
137+ <small>${ error ? 'Error loading email addresses!' : 'Feel free to reach out via the active addresses.' } </small>
138+ </p>
139+ ` ;
140+ return container ;
141+ }
142+
143+ // --- Collect elements to toggle ---
144+ const elementsToToggle = [
145+ mainBlockquote ,
146+ ...paperItems ,
147+ footer
148+ ] . filter ( el => el !== null ) ;
149+
150+ // --- State variables ---
151+ let showingEmails = false ;
152+ let emailBlockquote = null ; // will hold the DOM element after fetch
153+ let emailDataPromise = null ; // cache the fetch promise
154+
155+ // --- Insert the email block (called after data is ready) ---
156+ function insertEmailBlock ( emailData ) {
157+ if ( emailBlockquote ) return emailBlockquote ; // already inserted
158+
159+ emailBlockquote = createEmailBlockquote ( emailData ) ;
160+ const insertionPoint = mainBlockquote || document . querySelector ( '.col-xs-12' ) ;
161+ if ( insertionPoint ) {
162+ insertionPoint . insertAdjacentElement ( 'afterend' , emailBlockquote ) ;
163+ }
164+ return emailBlockquote ;
165+ }
166+
167+ // --- Toggle view handler ---
168+ async function toggleView ( event ) {
169+ event . preventDefault ( ) ;
170+
171+ // If we're about to show emails and haven't loaded them yet, fetch now
172+ if ( ! showingEmails && ! emailDataPromise ) {
173+ // Show a temporary loading state (optional)
174+ toggleBtn . textContent = '...' ;
175+
176+ const timeout = new Promise ( ( _ , reject ) =>
177+ setTimeout ( ( ) => reject ( new Error ( 'Request timed out' ) ) , 3000 )
178+ ) ;
179+
180+ emailDataPromise = Promise . race ( [
181+ fetch ( '/emails.json' ) . then ( response => {
182+ if ( ! response . ok ) throw new Error ( 'Failed to load email data' ) ;
183+ return response . json ( ) ;
184+ } ) ,
185+ timeout
186+ ] )
187+ . catch ( error => {
188+ console . error ( 'Could not load emails:' , error ) ;
189+ return { active : [ ] , inactive : [ ] , error : true } ;
190+ } ) ;
191+ }
192+
193+ // Wait for the data if we're opening the view for the first time
194+ if ( ! showingEmails && emailDataPromise ) {
195+ const emailData = await emailDataPromise ;
196+ insertEmailBlock ( emailData ) ;
197+ }
198+
199+ // Toggle visibility
200+ if ( emailBlockquote ) {
201+ emailBlockquote . classList . toggle ( 'hidden-view' ) ;
202+ }
203+ elementsToToggle . forEach ( el => el . classList . toggle ( 'hidden-view' ) ) ;
204+
205+ // Update button text
206+ toggleBtn . textContent = showingEmails ? toggleText . default : toggleText . alternate ;
207+
208+ // Flip state
209+ showingEmails = ! showingEmails ;
210+ }
211+
212+ // --- Attach event listener ---
213+ toggleBtn . addEventListener ( 'click' , toggleView ) ;
214+ } ) ;
0 commit comments