Skip to content

Commit c765ac3

Browse files
committed
resolve merge conflicts
2 parents 0519f6e + 9ea10c1 commit c765ac3

19 files changed

Lines changed: 1762 additions & 453 deletions

File tree

projects/contributor/index.html

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width,initial-scale=1" />
6+
<title>Contributors — vanilla-verse</title>
7+
<link rel="stylesheet" href="styles.css" />
8+
<meta name="description" content="Contributors page for commitra/vanilla-verse — avatars, contributions, links." />
9+
</head>
10+
<body>
11+
<main class="page">
12+
<header class="hero">
13+
<div class="title-block">
14+
<h1 class="title">Contributors</h1>
15+
<p class="subtitle">Thanks to everyone who contributes to <strong>commitra/vanilla-verse</strong></p>
16+
</div>
17+
18+
<div class="controls">
19+
<div class="control-row">
20+
<input id="search" type="search" placeholder="Search contributors..." aria-label="Search contributors" />
21+
<select id="sort" aria-label="Sort contributors">
22+
<option value="contributions-desc">Top contributors</option>
23+
<option value="contributions-asc">Least contributions</option>
24+
<option value="name-asc">Name A → Z</option>
25+
<option value="name-desc">Name Z → A</option>
26+
</select>
27+
</div>
28+
<div class="meta">
29+
<small id="count">Loading...</small>
30+
</div>
31+
</div>
32+
</header>
33+
34+
<section id="grid" class="grid" aria-live="polite">
35+
<!-- Cards injected here -->
36+
</section>
37+
38+
<footer class="foot">
39+
<p>
40+
Powered by GitHub API — <a href="https://github.com/commitra/vanilla-verse" target="_blank" rel="noopener">commitra/vanilla-verse</a>
41+
</p>
42+
<p class="hint">Tip: If you hit rate limits, add a personal token in <code>contributors.js</code> (see comment).</p>
43+
</footer>
44+
</main>
45+
46+
<script src="script.js" type="module"></script>
47+
</body>
48+
</html>

projects/contributor/script.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// contributors.js
2+
// Minimal, dependency-free module to fetch & render contributors for commitra/vanilla-verse
3+
// Place alongside index.html and styles.css and open index.html in a browser.
4+
// --- Notes ---
5+
// To avoid GitHub API rate limits for unauthenticated requests, provide a token:
6+
// 1) Create a personal access token with "public_repo" (no scopes needed for public data).
7+
// 2) Paste it below as `GITHUB_TOKEN` or set in localStorage under key "GH_TOKEN".
8+
// Keep tokens private — don't commit them to public repos. For production use – use server-side token proxy.
9+
10+
const OWNER = 'commitra';
11+
const REPO = 'vanilla-verse';
12+
13+
const GITHUB_TOKEN = ''; // <-- optional: paste token here (or store in localStorage 'GH_TOKEN')
14+
15+
const API_URL = `https://api.github.com/repos/${OWNER}/${REPO}/contributors?per_page=100`;
16+
17+
/** Utility: small GitHub icon (SVG) */
18+
function githubIconSVG() {
19+
return `<svg class="icon" viewBox="0 0 24 24" fill="none" aria-hidden="true" xmlns="http://www.w3.org/2000/svg">
20+
<path d="M12 .5C5.73.5.98 5.24.98 11.5c0 4.64 3.01 8.58 7.19 9.97.53.1.72-.23.72-.51 0-.25-.01-.91-.01-1.79-2.92.64-3.54-1.4-3.54-1.4-.48-1.22-1.17-1.55-1.17-1.55-.96-.66.07-.65.07-.65 1.06.08 1.62 1.09 1.62 1.09.94 1.6 2.48 1.14 3.08.87.09-.68.37-1.14.67-1.4-2.33-.27-4.78-1.16-4.78-5.17 0-1.14.41-2.07 1.08-2.8-.11-.27-.47-1.36.1-2.83 0 0 .88-.28 2.88 1.07A9.9 9.9 0 0112 6.8c.89.004 1.78.12 2.62.35 2-.35 2.88-1.07 2.88-1.07.57 1.47.21 2.56.1 2.83.67.73 1.08 1.66 1.08 2.8 0 4.02-2.46 4.89-4.8 5.15.38.33.72.98.72 1.98 0 1.43-.01 2.58-.01 2.94 0 .28.19.61.73.51 4.18-1.39 7.19-5.33 7.19-9.97C23.02 5.24 18.27.5 12 .5z" fill="currentColor"/>
21+
</svg>`;
22+
}
23+
24+
/** Simple loading skeleton card */
25+
function skeletonCard() {
26+
const el = document.createElement('div');
27+
el.className = 'card';
28+
el.innerHTML = `
29+
<div class="avatar" aria-hidden="true"><div style="width:100%;height:100%;background:linear-gradient(90deg,#0b2134,#0b2842)"></div></div>
30+
<div class="info" style="min-width:0">
31+
<div style="width:80%;height:14px;background:rgba(255,255,255,0.04);border-radius:8px"></div>
32+
<div style="height:10px;margin-top:10px;width:40%;background:rgba(255,255,255,0.02);border-radius:6px"></div>
33+
</div>`;
34+
return el;
35+
}
36+
37+
/** Render contributors array */
38+
function renderContributors(list) {
39+
const grid = document.getElementById('grid');
40+
grid.innerHTML = '';
41+
if (!Array.isArray(list) || list.length === 0) {
42+
grid.innerHTML = `<div class="empty">No contributors found.</div>`;
43+
document.getElementById('count').textContent = '0 contributors';
44+
return;
45+
}
46+
47+
document.getElementById('count').textContent = `${list.length} contributor${list.length>1 ? 's':''}`;
48+
49+
list.forEach(user => {
50+
const card = document.createElement('article');
51+
card.className = 'card';
52+
const username = user.login || 'unknown';
53+
const profileUrl = user.html_url || `https://github.com/${username}`;
54+
const avatar = user.avatar_url || '';
55+
const contributions = user.contributions ?? 0;
56+
57+
card.innerHTML = `
58+
<div class="avatar" title="${username}">
59+
<img loading="lazy" alt="${username}'s avatar" src="${avatar}" />
60+
</div>
61+
<div class="info">
62+
<div class="name">
63+
<a href="${profileUrl}" target="_blank" rel="noopener noreferrer" class="link" style="color:inherit;text-decoration:none;">
64+
<span>${username}</span>
65+
</a>
66+
<span class="badge" aria-hidden="true">${githubIconSVG()}<span style="margin-left:6px">${contributions}</span></span>
67+
</div>
68+
<div class="username">@${username}</div>
69+
</div>
70+
`;
71+
grid.appendChild(card);
72+
});
73+
}
74+
75+
/** Fetch contributors from GitHub API */
76+
async function fetchContributors() {
77+
const grid = document.getElementById('grid');
78+
grid.innerHTML = '';
79+
// show some skeletons
80+
for (let i=0;i<6;i++) grid.appendChild(skeletonCard());
81+
82+
// token fallback: localStorage > in-file constant
83+
const maybeToken = localStorage.getItem('GH_TOKEN') || GITHUB_TOKEN || '';
84+
const headers = { 'Accept': 'application/vnd.github.v3+json' };
85+
if (maybeToken) headers['Authorization'] = `token ${maybeToken}`;
86+
87+
try {
88+
const resp = await fetch(API_URL, { headers });
89+
if (resp.status === 403) {
90+
// Rate limited or forbidden
91+
const reset = resp.headers.get('x-ratelimit-reset');
92+
let msg = 'Rate limit exceeded or access forbidden.';
93+
if (reset) {
94+
const when = new Date(parseInt(reset,10)*1000);
95+
msg += ` Rate limit resets at ${when.toLocaleString()}.`;
96+
}
97+
grid.innerHTML = `<div class="empty">${msg} <br/>Tip: add a token to avoid limits.</div>`;
98+
document.getElementById('count').textContent = '0 contributors';
99+
return [];
100+
}
101+
if (!resp.ok) {
102+
const text = await resp.text();
103+
grid.innerHTML = `<div class="empty">Failed to load contributors. (${resp.status})</div>`;
104+
console.error('GitHub API error', resp.status, text);
105+
document.getElementById('count').textContent = '0 contributors';
106+
return [];
107+
}
108+
const data = await resp.json();
109+
// data is an array of contributors; map to consistent fields
110+
const mapped = data.map(u => ({
111+
login: u.login,
112+
html_url: u.html_url,
113+
avatar_url: u.avatar_url,
114+
contributions: u.contributions
115+
}));
116+
renderContributors(mapped);
117+
return mapped;
118+
} catch (err) {
119+
console.error(err);
120+
grid.innerHTML = `<div class="empty">Network error while fetching contributors.</div>`;
121+
document.getElementById('count').textContent = '0 contributors';
122+
return [];
123+
}
124+
}
125+
126+
/** Helpers: search & sort */
127+
function applySearchSort(data) {
128+
const q = document.getElementById('search').value.trim().toLowerCase();
129+
const sort = document.getElementById('sort').value;
130+
131+
let filtered = data.filter(u => {
132+
if (!q) return true;
133+
return (u.login && u.login.toLowerCase().includes(q));
134+
});
135+
136+
if (sort === 'contributions-desc') filtered.sort((a,b)=> (b.contributions||0) - (a.contributions||0));
137+
if (sort === 'contributions-asc') filtered.sort((a,b)=> (a.contributions||0) - (b.contributions||0));
138+
if (sort === 'name-asc') filtered.sort((a,b)=> (a.login||'').localeCompare(b.login||''));
139+
if (sort === 'name-desc') filtered.sort((a,b)=> (b.login||'').localeCompare(a.login||''));
140+
renderContributors(filtered);
141+
}
142+
143+
/** Bind UI */
144+
async function init() {
145+
const data = await fetchContributors();
146+
// attach listeners
147+
document.getElementById('search').addEventListener('input', () => applySearchSort(data));
148+
document.getElementById('sort').addEventListener('change', () => applySearchSort(data));
149+
150+
// keyboard shortcut: press "t" to toggle token prompt (for dev use)
151+
document.addEventListener('keydown', (e)=>{
152+
if (e.key === 'T' || e.key === 't') {
153+
const current = localStorage.getItem('GH_TOKEN') || '';
154+
const newToken = prompt('Enter GitHub token (will be stored in localStorage for this page). Leave blank to clear.', current);
155+
if (newToken === null) return;
156+
if (newToken.trim()) {
157+
localStorage.setItem('GH_TOKEN', newToken.trim());
158+
alert('Token saved to localStorage. Refreshing...');
159+
} else {
160+
localStorage.removeItem('GH_TOKEN');
161+
alert('Token cleared. Refreshing...');
162+
}
163+
window.location.reload();
164+
}
165+
});
166+
}
167+
168+
document.addEventListener('DOMContentLoaded', init);

0 commit comments

Comments
 (0)