Skip to content

Commit 4f2d253

Browse files
Add monthly featured developer showcase (#187)
* Add monthly featured developer showcase - Add engagement scoring algorithm (followers, stars, repos, sponsors, activity) - Auto-select top user each month based on calculated score - Add featured user section with gradient banner in layout - Display avatar, stats, location, and top languages - Responsive design for mobile/tablet/desktop - Add generate_featured.py to regenerate featured user on demand - Generate featured.json with sindresorhus as first featured developer * Fix DeepSource issues: remove console calls and fix logging f-strings - Remove console.log and console.error from loadFeaturedUser (JS-0002) - Convert all logging f-strings to parameter substitution (PYL-W1203) - Featured user loading now fails silently if not available * Fix final logging f-string in select_featured_user * Address Gemini code review feedback - Refactored loadFeaturedUser() into smaller, focused functions: - renderFeaturedUser() for DOM updates - getFeaturedUserElements() for element retrieval - updateFeaturedAvatar(), updateFeaturedInfo(), updateFeaturedStats(), updateFeaturedLanguages() for specific updates - Moved datetime imports to top of file (PEP 8) - Created constants for engagement score weights and thresholds - Fixed DRY violation in generate_featured.py by importing save_featured_user - Changed broad Exception to specific (IOError, OSError) - Added final newline to featured.json output All code quality improvements address Gemini bot's code review on PR #187. * Fix Python linting issues - Remove duplicate return statement (PYL-W0101) - Use OSError instead of overlapping (IOError, OSError) (PYL-W0714) * Apply black and isort formatting --------- Co-authored-by: John Bampton <jbampton@users.noreply.github.com>
1 parent df55d45 commit 4f2d253

6 files changed

Lines changed: 467 additions & 3 deletions

File tree

docs/featured.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"user":{"login":"sindresorhus","id":170270,"node_id":"MDQ6VXNlcjE3MDI3MA==","avatar_url":"https://avatars.githubusercontent.com/u/170270?v=4","gravatar_id":"","url":"https://api.github.com/users/sindresorhus","html_url":"https://github.com/sindresorhus","followers_url":"https://api.github.com/users/sindresorhus/followers","following_url":"https://api.github.com/users/sindresorhus/following{/other_user}","gists_url":"https://api.github.com/users/sindresorhus/gists{/gist_id}","starred_url":"https://api.github.com/users/sindresorhus/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/sindresorhus/subscriptions","organizations_url":"https://api.github.com/users/sindresorhus/orgs","repos_url":"https://api.github.com/users/sindresorhus/repos","events_url":"https://api.github.com/users/sindresorhus/events{/privacy}","received_events_url":"https://api.github.com/users/sindresorhus/received_events","type":"User","user_view_type":"public","site_admin":false,"score":1.0,"followers":76521,"following":31,"location":null,"name":"Sindre Sorhus","public_repos":1129,"public_gists":99,"sponsors_count":202,"sponsoring_count":13,"avatar_updated_at":"2025-12-20T02:20:38Z","followers_previous":null,"followers_growth_pct":null,"followers_snapshot_at":1766716761,"top_languages":[{"name":"JavaScript","bytes":5377121,"percent":55.4},{"name":"TypeScript","bytes":2702401,"percent":27.8},{"name":"Swift","bytes":1017726,"percent":10.5},{"name":"Rust","bytes":172395,"percent":1.8},{"name":"CSS","bytes":139381,"percent":1.4}],"total_stars":867226,"last_repo_pushed_at":"2025-12-25T18:17:03Z","last_public_commit_at":"2025-12-25T18:24:07Z"},"selected_at":"2025-12-26T07:20:25.156359","month":"December 2025"}

docs/script.js

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ document.addEventListener('DOMContentLoaded', initializeApp);
4343
async function initializeApp() {
4444
showLoadingState();
4545
setupEventListeners();
46-
await fetchAndPrepareUsers();
46+
await Promise.all([fetchAndPrepareUsers(), loadFeaturedUser()]);
4747

4848
// Do these if data is loaded
4949
if (isDataLoaded) {
@@ -164,6 +164,110 @@ async function fetchAndPrepareUsers() {
164164
}
165165
}
166166

167+
/**
168+
* Load and display the featured user of the month
169+
*/
170+
async function loadFeaturedUser() {
171+
try {
172+
const res = await fetch('featured.json', { cache: 'no-store' });
173+
if (!res.ok) {
174+
return;
175+
}
176+
const data = await res.json();
177+
if (data?.user?.login) {
178+
renderFeaturedUser(data.user);
179+
}
180+
} catch (err) {
181+
// Silently fail - featured user is optional
182+
}
183+
}
184+
185+
/**
186+
* Render featured user to DOM
187+
*/
188+
function renderFeaturedUser(user) {
189+
const elements = getFeaturedUserElements();
190+
if (!elements.section) return;
191+
192+
updateFeaturedAvatar(elements, user);
193+
updateFeaturedInfo(elements, user);
194+
updateFeaturedStats(elements, user);
195+
updateFeaturedLanguages(elements, user);
196+
197+
elements.section.style.display = 'block';
198+
}
199+
200+
/**
201+
* Get all featured user DOM elements
202+
*/
203+
function getFeaturedUserElements() {
204+
return {
205+
section: document.getElementById('featuredUserSection'),
206+
avatar: document.getElementById('featuredUserAvatar'),
207+
link: document.getElementById('featuredUserLink'),
208+
name: document.getElementById('featuredUserName'),
209+
loginLink: document.getElementById('featuredUserLoginLink'),
210+
location: document.getElementById('featuredUserLocation'),
211+
followers: document.getElementById('featuredUserFollowers'),
212+
stars: document.getElementById('featuredUserStars'),
213+
repos: document.getElementById('featuredUserRepos'),
214+
sponsors: document.getElementById('featuredUserSponsors'),
215+
languages: document.getElementById('featuredUserLanguages'),
216+
};
217+
}
218+
219+
/**
220+
* Update featured user avatar and links
221+
*/
222+
function updateFeaturedAvatar(elements, user) {
223+
const avatarUrl = `images/faces/${user.login.toLowerCase()}.png`;
224+
elements.avatar.src = avatarUrl;
225+
elements.avatar.alt = `${user.login}'s avatar`;
226+
elements.link.href = user.html_url;
227+
elements.loginLink.href = user.html_url;
228+
elements.loginLink.textContent = `@${user.login}`;
229+
}
230+
231+
/**
232+
* Update featured user info (name and location)
233+
*/
234+
function updateFeaturedInfo(elements, user) {
235+
elements.name.textContent = user.name || user.login;
236+
237+
if (user.location) {
238+
elements.location.querySelector('.location-text').textContent =
239+
user.location;
240+
elements.location.style.display = 'flex';
241+
} else {
242+
elements.location.style.display = 'none';
243+
}
244+
}
245+
246+
/**
247+
* Update featured user stats
248+
*/
249+
function updateFeaturedStats(elements, user) {
250+
elements.followers.textContent = formatDisplay(user.followers);
251+
elements.stars.textContent = formatDisplay(user.total_stars);
252+
elements.repos.textContent = formatDisplay(user.public_repos);
253+
elements.sponsors.textContent = formatDisplay(user.sponsors_count);
254+
}
255+
256+
/**
257+
* Update featured user languages
258+
*/
259+
function updateFeaturedLanguages(elements, user) {
260+
elements.languages.innerHTML = '';
261+
if (Array.isArray(user.top_languages) && user.top_languages.length > 0) {
262+
user.top_languages.slice(0, 5).forEach((lang) => {
263+
const badge = document.createElement('div');
264+
badge.className = 'featured-language-badge';
265+
badge.textContent = lang.name;
266+
elements.languages.appendChild(badge);
267+
});
268+
}
269+
}
270+
167271
function prepareUserFromJson(user) {
168272
const getNum = (v, def = 0) =>
169273
v === 'N/A' || v == null ? def : parseInt(v, 10);

docs/styles.css

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,193 @@ h1 {
380380
padding-right: 20px;
381381
}
382382

383+
/* ============================================================================ */
384+
/* FEATURED USER SECTION */
385+
/* ============================================================================ */
386+
.featured-user-section {
387+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
388+
border-radius: 16px;
389+
padding: 32px;
390+
margin: 20px auto;
391+
max-width: 1000px;
392+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
393+
position: relative;
394+
overflow: hidden;
395+
}
396+
397+
.featured-user-section::before {
398+
content: '';
399+
position: absolute;
400+
top: -50%;
401+
right: -50%;
402+
width: 200%;
403+
height: 200%;
404+
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
405+
animation: pulse 15s ease-in-out infinite;
406+
}
407+
408+
@keyframes pulse {
409+
0%, 100% {
410+
transform: scale(1);
411+
opacity: 0.5;
412+
}
413+
50% {
414+
transform: scale(1.1);
415+
opacity: 0.3;
416+
}
417+
}
418+
419+
.featured-badge {
420+
display: inline-block;
421+
background: rgba(255, 255, 255, 0.2);
422+
backdrop-filter: blur(10px);
423+
padding: 8px 20px;
424+
border-radius: 20px;
425+
font-size: 0.9rem;
426+
font-weight: 600;
427+
color: white;
428+
margin-bottom: 20px;
429+
border: 1px solid rgba(255, 255, 255, 0.3);
430+
}
431+
432+
.featured-content {
433+
display: flex;
434+
gap: 32px;
435+
align-items: center;
436+
position: relative;
437+
z-index: 1;
438+
}
439+
440+
.featured-avatar-link {
441+
flex-shrink: 0;
442+
}
443+
444+
.featured-avatar {
445+
width: 150px;
446+
height: 150px;
447+
border-radius: 50%;
448+
border: 5px solid rgba(255, 255, 255, 0.3);
449+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
450+
transition: transform 0.3s ease;
451+
}
452+
453+
.featured-avatar:hover {
454+
transform: scale(1.05);
455+
}
456+
457+
.featured-info {
458+
flex: 1;
459+
color: white;
460+
}
461+
462+
.featured-name {
463+
font-size: 2rem;
464+
font-weight: 700;
465+
margin: 0 0 8px 0;
466+
color: white;
467+
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
468+
}
469+
470+
.featured-login {
471+
font-size: 1.2rem;
472+
color: rgba(255, 255, 255, 0.9);
473+
text-decoration: none;
474+
display: inline-block;
475+
margin-bottom: 12px;
476+
transition: color 0.2s;
477+
}
478+
479+
.featured-login:hover {
480+
color: white;
481+
text-decoration: underline;
482+
}
483+
484+
.featured-location {
485+
display: flex;
486+
align-items: center;
487+
gap: 6px;
488+
font-size: 1rem;
489+
color: rgba(255, 255, 255, 0.9);
490+
margin-bottom: 20px;
491+
}
492+
493+
.featured-stats {
494+
display: grid;
495+
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
496+
gap: 20px;
497+
margin: 20px 0;
498+
}
499+
500+
.featured-stat {
501+
text-align: center;
502+
background: rgba(255, 255, 255, 0.15);
503+
backdrop-filter: blur(10px);
504+
padding: 16px;
505+
border-radius: 12px;
506+
border: 1px solid rgba(255, 255, 255, 0.2);
507+
}
508+
509+
.featured-stat .stat-value {
510+
font-size: 1.8rem;
511+
font-weight: 700;
512+
color: white;
513+
margin-bottom: 4px;
514+
}
515+
516+
.featured-stat .stat-label {
517+
font-size: 0.85rem;
518+
color: rgba(255, 255, 255, 0.85);
519+
text-transform: uppercase;
520+
letter-spacing: 0.5px;
521+
font-weight: 500;
522+
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.2);
523+
}
524+
525+
.featured-languages {
526+
display: flex;
527+
flex-wrap: wrap;
528+
gap: 8px;
529+
margin-top: 16px;
530+
}
531+
532+
.featured-language-badge {
533+
background: rgba(255, 255, 255, 0.2);
534+
backdrop-filter: blur(10px);
535+
padding: 6px 14px;
536+
border-radius: 16px;
537+
font-size: 0.85rem;
538+
color: white;
539+
font-weight: 500;
540+
border: 1px solid rgba(255, 255, 255, 0.3);
541+
}
542+
543+
@media (max-width: 768px) {
544+
.featured-user-section {
545+
padding: 24px;
546+
margin: 10px;
547+
}
548+
549+
.featured-content {
550+
flex-direction: column;
551+
text-align: center;
552+
gap: 20px;
553+
}
554+
555+
.featured-avatar {
556+
width: 120px;
557+
height: 120px;
558+
}
559+
560+
.featured-name {
561+
font-size: 1.5rem;
562+
}
563+
564+
.featured-stats {
565+
grid-template-columns: repeat(2, 1fr);
566+
gap: 12px;
567+
}
568+
}
569+
383570
.grid {
384571
display: flex;
385572
flex-wrap: wrap;

0 commit comments

Comments
 (0)