Skip to content

Commit 0fa4e4e

Browse files
committed
New Citations Logic
1 parent d22635c commit 0fa4e4e

5 files changed

Lines changed: 361 additions & 56 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Update citation counts
2+
3+
on:
4+
schedule:
5+
- cron: "0 4 * * 1" # every Monday at 04:00 UTC
6+
workflow_dispatch: # allow manual runs from the Actions tab
7+
8+
jobs:
9+
fetch-citations:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write # needed to push citations.json back to the repo
13+
14+
steps:
15+
- name: Checkout repo
16+
uses: actions/checkout@v4
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: "3.12"
22+
23+
- name: Install dependencies
24+
run: pip install scholarly
25+
26+
- name: Fetch citation counts
27+
run: python fetch_citations.py
28+
# If Google blocks the run, set up a proxy here instead:
29+
# env:
30+
# SCHOLARLY_USE_PROXY: "luminati" # or another supported proxy
31+
# See: https://scholarly.readthedocs.io/en/stable/ProxyGenerator.html
32+
33+
- name: Commit and push citations.json
34+
run: |
35+
git config user.name "github-actions[bot]"
36+
git config user.email "github-actions[bot]@users.noreply.github.com"
37+
git add citations.json
38+
# Only commit if there are actual changes
39+
git diff --cached --quiet || git commit -m "chore: update citation counts [skip ci]"
40+
git push

assets/old-script.js

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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+
});

assets/script.js

Lines changed: 27 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,40 @@ document.addEventListener("DOMContentLoaded", () => {
33
papers.forEach((paper, index) => {
44
paper.style.animationDelay = `${index * 0.25}s`;
55
});
6-
6+
77
const citationElements = document.querySelectorAll('.citation[data-doi]');
88
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) {
9+
10+
// Path to the static file committed by GitHub Actions.
11+
// Adjust if your site root differs (e.g. "/assets/citations.json").
12+
const CITATIONS_URL = "/citations.json";
13+
14+
async function run() {
15+
let data;
2916
try {
30-
localStorage.setItem(CACHE_KEY, JSON.stringify({ timestamp: Date.now(), data }));
31-
} catch {
32-
// localStorage full or unavailable — fail silently
17+
const res = await fetch(CITATIONS_URL);
18+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
19+
data = await res.json(); // { "10.xxxx/yyyy": 42, ... }
20+
} catch (err) {
21+
console.warn("Could not load citations.json:", err);
22+
return;
3323
}
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;
24+
4825
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;
26+
27+
citationElements.forEach((el) => {
28+
const doi = el.getAttribute("data-doi");
29+
if (doi in data) {
30+
const count = data[doi];
5731
citationCounts.push(count);
5832
el.textContent = `‒ CITATIONS: ${count} ‒`;
59-
} catch {
60-
el.textContent = '';
61-
anyFailed = true;
33+
} else {
34+
el.textContent = "";
6235
}
63-
}));
64-
65-
// Save whatever we successfully got
66-
if (Object.keys(freshData).length) saveCache(freshData);
67-
36+
});
37+
6838
// Update total + h-index
69-
if (totalDisplay && !anyFailed) {
39+
if (totalDisplay && citationCounts.length) {
7040
const sorted = citationCounts.slice().sort((a, b) => b - a);
7141
let hIndex = 0;
7242
for (let i = 0; i < sorted.length; i++) {
@@ -77,9 +47,10 @@ document.addEventListener("DOMContentLoaded", () => {
7747
totalDisplay.textContent = `‒ CITATIONS: ${total} · H-INDEX: ${hIndex} ‒`;
7848
}
7949
}
80-
50+
8151
run();
8252

53+
8354
const toggleText = {
8455
default: 'CONTACT',
8556
alternate: 'HOME'

citations.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"10.1016/j.heliyon.2024.e31414": 3,
3+
"10.1038/s41598-024-62747-z": 9,
4+
"10.1007/s10827-025-00915-4": 0
5+
}

0 commit comments

Comments
 (0)