Skip to content

Commit ab498be

Browse files
committed
updated and simplified JS
1 parent ed6aad9 commit ab498be

1 file changed

Lines changed: 120 additions & 118 deletions

File tree

assets/script.js

Lines changed: 120 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
11
document.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

Comments
 (0)