Skip to content

Commit c2168da

Browse files
web: contributors page, real avatars, bookmark redesign
- New /contributors page listing all 240 contributors (avatar, name, GitHub profile link), generated from the README contributors block by scripts/gen-contributors.mjs. - Replace initial-letter avatars with real GitHub avatars on the homepage. Use direct avatars.githubusercontent.com URLs so they satisfy the site's COEP: require-corp header — github.com/<handle>.png 302-redirects and the redirect is blocked under COEP. - Expand the homepage contributor wall from 12 to 30 cards; cards link out to GitHub profiles. - Redesign the nav bookmark control as a labelled toggle button (lucide icon + count) with a clear active state, replacing the bare circle. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent afb8fe3 commit c2168da

7 files changed

Lines changed: 1439 additions & 26 deletions

File tree

web/scripts/gen-contributors.mjs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Generates src/lib/contributors.json — every contributor, scraped from the
2+
// contrib-readme-action block in the repo's README.md (handle, display name,
3+
// GitHub avatar URL).
4+
//
5+
// Run from the web/ directory: node scripts/gen-contributors.mjs
6+
// Re-run whenever the README's contributors list changes.
7+
8+
import fs from "fs";
9+
import path from "path";
10+
11+
const WEB = process.cwd();
12+
const README = path.join(WEB, "..", "README.md");
13+
const OUT = path.join(WEB, "src", "lib", "contributors.json");
14+
15+
const BOTS = new Set(["lint-action", "github-actions", "dependabot"]);
16+
17+
const md = fs.readFileSync(README, "utf8");
18+
const start = md.indexOf("<!-- readme: contributors -start -->");
19+
const end = md.indexOf("<!-- readme: contributors -end -->");
20+
if (start < 0 || end < 0) {
21+
throw new Error("contributors block not found in README.md");
22+
}
23+
const block = md.slice(start, end);
24+
25+
// Each entry: <a href="…/HANDLE"> <img src="AVATAR" … /> <br/> <sub><b>NAME</b></sub>
26+
const re =
27+
/<a href="https:\/\/github\.com\/([^"/]+)">\s*<img src="([^"]+)"[^>]*\/>\s*<br\s*\/>\s*<sub><b>([^<]+)<\/b><\/sub>/g;
28+
29+
const seen = new Set();
30+
const contributors = [];
31+
let m;
32+
while ((m = re.exec(block))) {
33+
const handle = m[1].trim();
34+
const key = handle.toLowerCase();
35+
if (seen.has(key) || BOTS.has(key) || key.endsWith("[bot]")) continue;
36+
seen.add(key);
37+
contributors.push({ handle, name: m[3].trim(), avatar: m[2].trim() });
38+
}
39+
40+
fs.writeFileSync(OUT, JSON.stringify(contributors, null, 2) + "\n");
41+
console.log(`contributors.json: ${contributors.length} contributors`);

web/src/app/contributors/page.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import type { Metadata } from "next";
2+
import SiteNav from "@/components/SiteNav";
3+
import SiteFooter from "@/components/SiteFooter";
4+
import { REPO_URL } from "@/lib/data";
5+
import type { RepoContributor } from "@/types";
6+
import contributors from "@/lib/contributors.json";
7+
8+
// The full contributor roll, scraped from the README by scripts/gen-contributors.mjs.
9+
const CONTRIBUTORS = contributors as RepoContributor[];
10+
11+
export const metadata: Metadata = {
12+
title: "Contributors — pyBegin",
13+
description: `The ${CONTRIBUTORS.length} people who built the pyBegin beginner-project collection.`,
14+
};
15+
16+
export default function ContributorsPage() {
17+
return (
18+
<div className="s-shell">
19+
<SiteNav />
20+
21+
<header className="s-crew-head">
22+
<h1 className="s-crew-title">
23+
Made by <em>{CONTRIBUTORS.length}</em> humans.
24+
</h1>
25+
<p className="s-crew-sub">
26+
Everyone who has contributed a project or a fix to this open-source
27+
collection. Your face could be here next —{" "}
28+
<a href={`${REPO_URL}/blob/main/CONTRIBUTING.md`}>
29+
start with the contributing guide
30+
</a>
31+
.
32+
</p>
33+
</header>
34+
35+
<div className="s-crew-grid">
36+
{CONTRIBUTORS.map((c) => (
37+
<a
38+
key={c.handle}
39+
className="s-crew-card"
40+
href={`https://github.com/${c.handle}`}
41+
target="_blank"
42+
rel="noreferrer"
43+
title={`${c.name} — @${c.handle}`}
44+
>
45+
<img
46+
className="s-crew-avatar"
47+
src={`${c.avatar}&s=132`}
48+
alt=""
49+
width={66}
50+
height={66}
51+
loading="lazy"
52+
/>
53+
<div className="s-crew-name">{c.name}</div>
54+
<div className="s-crew-handle">@{c.handle}</div>
55+
</a>
56+
))}
57+
</div>
58+
59+
<SiteFooter />
60+
</div>
61+
);
62+
}

web/src/app/globals.css

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,24 @@ a { color: inherit; }
8282
.s-navlinks a.active, .s-navlinks button.active { background: var(--s-ink); color: var(--s-bg); }
8383
.s-navlinks a:hover:not(.active), .s-navlinks button:hover:not(.active) { background: rgba(29,24,48, 0.08); }
8484
.s-nav-right { display: flex; gap: 10px; align-items: center; }
85-
.s-bm-count {
86-
width: 38px; height: 38px; border-radius: 50%;
85+
.s-bm-btn {
86+
display: inline-flex; align-items: center; gap: 6px;
87+
height: 38px; padding: 0 14px; border-radius: var(--s-radius-pill);
8788
background: var(--s-surface); border: var(--s-border-thin);
88-
display: grid; place-items: center; font-weight: 700; font-size: 14px;
89-
box-shadow: 3px 3px 0 var(--s-ink); position: relative; cursor: pointer;
90-
}
91-
.s-bm-count.has::after {
92-
content: ''; position: absolute; top: 4px; right: 4px;
93-
width: 8px; height: 8px; border-radius: 50%; background: var(--s-accent);
94-
border: 1.5px solid var(--s-ink);
95-
}
89+
box-shadow: 3px 3px 0 var(--s-ink);
90+
font-weight: 800; font-size: 13px; color: var(--s-ink); cursor: pointer;
91+
transition: transform .1s, box-shadow .1s, background .12s, color .12s;
92+
}
93+
.s-bm-btn:hover { transform: translate(-1px, -1px); box-shadow: 4px 4px 0 var(--s-ink); }
94+
.s-bm-btn:active { transform: translate(2px, 2px); box-shadow: 0 0 0 var(--s-ink); }
95+
.s-bm-btn.active { background: var(--s-ink); color: var(--s-bg); }
96+
.s-bm-n { font-family: var(--s-mono); font-size: 12.5px; font-weight: 800; }
97+
.s-bm-btn.active .s-bm-n { color: var(--s-accent); }
98+
.s-bm-word {
99+
font-size: 11px; font-weight: 800; letter-spacing: 0.04em;
100+
text-transform: uppercase;
101+
}
102+
@media (max-width: 520px) { .s-bm-word { display: none; } }
96103
.s-cta {
97104
padding: 12px 22px; border-radius: var(--s-radius-pill);
98105
background: var(--s-ink); color: var(--s-bg);
@@ -390,6 +397,56 @@ a { color: inherit; }
390397
background: var(--s-ink); color: var(--s-bg); font-size: 11px; font-weight: 800;
391398
letter-spacing: 0.08em; text-transform: uppercase; border-radius: var(--s-radius-pill);
392399
}
400+
/* real GitHub avatars (replace the initials) */
401+
img.s-avatar { display: block; object-fit: cover; padding: 0; }
402+
a.s-contrib { display: block; text-decoration: none; color: inherit; }
403+
404+
/* ---- /contributors — the full crew page ---- */
405+
.s-crew-head { margin: 40px 0 28px; max-width: 720px; }
406+
.s-crew-title {
407+
font-family: var(--s-display); font-weight: 900;
408+
font-size: clamp(34px, 5vw, 56px); letter-spacing: -0.03em; line-height: 1;
409+
}
410+
.s-crew-title em {
411+
font-style: normal; color: var(--s-accent);
412+
-webkit-text-stroke: 1.5px var(--s-ink);
413+
}
414+
.s-crew-sub {
415+
margin-top: 14px; font-size: 16px; line-height: 1.55;
416+
color: rgba(29, 24, 48, 0.7);
417+
}
418+
.s-crew-sub a { color: var(--s-accent); font-weight: 700; }
419+
.s-crew-sub a:hover { text-decoration: underline; }
420+
.s-crew-grid {
421+
display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
422+
gap: 14px; margin-bottom: 56px;
423+
}
424+
.s-crew-card {
425+
display: flex; flex-direction: column; align-items: center; text-align: center;
426+
padding: 20px 12px; border-radius: 16px;
427+
background: var(--s-surface); border: var(--s-border-thin);
428+
box-shadow: 4px 4px 0 var(--s-ink);
429+
text-decoration: none; color: inherit;
430+
transition: transform .15s, box-shadow .15s;
431+
}
432+
.s-crew-card:hover {
433+
transform: translate(-2px, -2px); box-shadow: 6px 6px 0 var(--s-accent);
434+
}
435+
.s-crew-avatar {
436+
width: 66px; height: 66px; border-radius: 50%; object-fit: cover;
437+
border: var(--s-border-thin); background: var(--s-bg);
438+
}
439+
.s-crew-name {
440+
margin-top: 12px; font-weight: 800; font-size: 14px; line-height: 1.25;
441+
overflow-wrap: anywhere;
442+
}
443+
.s-crew-handle {
444+
margin-top: 3px; font-family: var(--s-mono); font-size: 11px;
445+
color: rgba(29, 24, 48, 0.55); overflow-wrap: anywhere;
446+
}
447+
@media (max-width: 640px) {
448+
.s-crew-grid { grid-template-columns: repeat(auto-fill, minmax(116px, 1fr)); }
449+
}
393450

394451
.s-more { display: flex; justify-content: center; margin-top: 28px; }
395452
.s-more button, .s-more a {

web/src/components/Home.tsx

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { useEffect, useMemo, useState } from "react";
77
import Link from "next/link";
8+
import { Bookmark } from "lucide-react";
89
import StickerLogo from "./StickerLogo";
910
import ScribbleSvg from "./ScribbleSvg";
1011
import HeroStickers from "./HeroStickers";
@@ -13,7 +14,21 @@ import ProjectModal from "./ProjectModal";
1314
import SiteFooter from "./SiteFooter";
1415
import { CATEGORIES, CONTRIBUTORS, PATHS, PROJECTS, REPO_URL, getProject } from "@/lib/data";
1516
import { getBookmarks, toggleBookmark } from "@/lib/bookmarks";
16-
import type { Project } from "@/types";
17+
import contributors from "@/lib/contributors.json";
18+
import type { Project, RepoContributor } from "@/types";
19+
20+
const CREW = contributors as RepoContributor[];
21+
22+
// Resolve a handle to its GitHub avatar. Uses the direct avatars.github URL
23+
// (not github.com/<handle>.png — that 302-redirects and the redirect is
24+
// blocked by the site's COEP: require-corp header).
25+
const AVATARS = new Map(
26+
contributors.map((c) => [c.handle.toLowerCase(), c.avatar]),
27+
);
28+
const avatarFor = (handle: string, size: number) => {
29+
const url = AVATARS.get(handle.toLowerCase());
30+
return url ? `${url}&s=${size}` : `https://github.com/${handle}.png`;
31+
};
1732

1833
const ACCENT = "#ff7a59";
1934
const SORTS = ["default", "alpha", "short", "bookmarks"] as const;
@@ -81,16 +96,29 @@ export default function Home() {
8196
<button onClick={() => scrollTo("crew")}>Contribute</button>
8297
</div>
8398
<div className="s-nav-right">
84-
<div
85-
className={`s-bm-count ${bm.length ? "has" : ""}`}
86-
title="Bookmarks"
99+
<button
100+
type="button"
101+
className={`s-bm-btn${showBmOnly ? " active" : ""}`}
102+
aria-pressed={showBmOnly}
103+
title={
104+
showBmOnly
105+
? "Showing saved projects — click to show all"
106+
: "Show your saved projects"
107+
}
87108
onClick={() => {
88109
setShowBmOnly((v) => !v);
89110
scrollTo("gallery");
90111
}}
91112
>
92-
<span style={{ fontSize: 11, marginLeft: 2 }}>{bm.length}</span>
93-
</div>
113+
<Bookmark
114+
size={15}
115+
strokeWidth={2.5}
116+
fill={bm.length ? "currentColor" : "none"}
117+
aria-hidden="true"
118+
/>
119+
<span className="s-bm-n">{bm.length}</span>
120+
<span className="s-bm-word">saved</span>
121+
</button>
94122
<a className="s-cta" href={REPO_URL} target="_blank" rel="noreferrer">
95123
★ Star · 2.3k
96124
</a>
@@ -305,7 +333,13 @@ export default function Home() {
305333
</div>
306334
<div className="s-contributors">
307335
<div className="s-contrib lead">
308-
<div className="s-avatar">{CONTRIBUTORS[0].name[0]}</div>
336+
<img
337+
className="s-avatar"
338+
src={avatarFor(CONTRIBUTORS[0].handle, 200)}
339+
alt=""
340+
width={84}
341+
height={84}
342+
/>
309343
<div className="s-cname">{CONTRIBUTORS[0].name}</div>
310344
<div className="s-chandle">@{CONTRIBUTORS[0].handle}</div>
311345
<p className="s-quote">
@@ -315,24 +349,34 @@ export default function Home() {
315349
</p>
316350
<div className="s-badge">Maintainer · 412 commits</div>
317351
</div>
318-
{CONTRIBUTORS.slice(1, 13).map((c) => (
319-
<div key={c.handle} className="s-contrib">
320-
<div className="s-avatar">{c.name[0]}</div>
352+
{CREW.slice(1, 31).map((c) => (
353+
<a
354+
key={c.handle}
355+
className="s-contrib"
356+
href={`https://github.com/${c.handle}`}
357+
target="_blank"
358+
rel="noreferrer"
359+
>
360+
<img
361+
className="s-avatar"
362+
src={`${c.avatar}&s=120`}
363+
alt=""
364+
width={56}
365+
height={56}
366+
loading="lazy"
367+
/>
321368
<div className="s-cname">{c.name.split(" ")[0]}</div>
322369
<div className="s-chandle">
323370
@
324371
{c.handle.length > 12
325372
? c.handle.slice(0, 12) + "…"
326373
: c.handle}
327374
</div>
328-
<div className="s-commits">{c.commits}</div>
329-
</div>
375+
</a>
330376
))}
331377
</div>
332378
<div className="s-more">
333-
<a href={`${REPO_URL}/graphs/contributors`}>
334-
See all 241 contributors →
335-
</a>
379+
<Link href="/contributors">See all contributors →</Link>
336380
</div>
337381
</section>
338382

0 commit comments

Comments
 (0)