Skip to content

Commit 7f61673

Browse files
committed
feat: source workbench cards from pinned repos
1 parent 5e35d2e commit 7f61673

5 files changed

Lines changed: 79 additions & 37 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
## About
1212

13-
Astro portfolio with a static export for GitHub Pages. It fetches public, non-fork repositories from the GitHub GraphQL API at build time, resolves live GitHub Pages links through the REST API, and renders them in a terminal-themed UI.
13+
Astro portfolio with a static export for GitHub Pages. It fetches the repositories pinned on Jonathan's GitHub profile plus public, non-fork repositories from the GitHub GraphQL API at build time, resolves live GitHub Pages links through the REST API, and renders them in a terminal-themed UI.
1414

1515
The site includes a print-optimized resume page, SEO metadata, analytics, and a Konami code easter egg. The same shared data powers the on-page resume and the dedicated `/resume` route.
1616

@@ -29,7 +29,8 @@ It is built to stay simple to deploy: build locally, export statically, and publ
2929

3030
## Features
3131

32-
- Dynamic Workbench repository ledger from GitHub GraphQL API (public, non-fork repos)
32+
- Workbench major cards sourced from GitHub profile pinned repositories
33+
- Dynamic "Other GitHub repos" ledger from GitHub GraphQL API (public, non-fork repos)
3334
- Live GitHub Pages links resolved at build time via GitHub REST API
3435
- Terminal-themed dark UI with typing animations and scroll effects
3536
- Print-optimized resume page with download support

src/components/Portfolio.tsx

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
AVAILABILITY,
66
ENGINEERING_PRINCIPLES,
77
EXPERIENCES,
8-
FEATURED_PROJECTS,
98
OPERATING_SIGNALS,
109
PROFILE,
1110
SKILLS,
@@ -151,18 +150,18 @@ const SKILL_GROUPS: Array<{ key: keyof typeof SKILLS; label: string; path: strin
151150
{ key: "frontend", label: "interface", path: "/stack/interface" },
152151
];
153152

154-
function projectLane(project: (typeof FEATURED_PROJECTS)[number]) {
155-
const text = `${project.name} ${project.tags.join(" ")}`.toLowerCase();
153+
function projectLane(project: GitHubRepo) {
154+
const text = `${project.title} ${project.description} ${project.lang}`.toLowerCase();
156155
if (text.includes("rinha") || text.includes("k6") || text.includes("performance")) return "load path";
157-
if (text.includes("clean") || text.includes(".net")) return "system design";
158-
if (text.includes("game") || text.includes("sdl") || text.includes("lynx")) return "runtime lab";
156+
if (text.includes("clean") || text.includes(".net") || text.includes("blazor")) return "system design";
157+
if (text.includes("game") || text.includes("sdl") || text.includes("lynx") || text.includes("mango")) return "runtime lab";
159158
return "field note";
160159
}
161160

162161
export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
163162
const scrollProgress = useScrollProgress();
164-
const featuredSlugs = useMemo(() => new Set(FEATURED_PROJECTS.map((fp) => fp.slug)), []);
165-
const workbenchRepos = useMemo(() => projects.filter((project) => !featuredSlugs.has(project.title)), [featuredSlugs, projects]);
163+
const pinnedRepos = useMemo(() => projects.filter((project) => project.pinned), [projects]);
164+
const otherRepos = useMemo(() => projects.filter((project) => !project.pinned), [projects]);
166165

167166
const [termOpen, setTermOpen] = useState(false);
168167
const [termInput, setTermInput] = useState("");
@@ -378,19 +377,24 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
378377
<SectionLabel id="workbench-heading" number="04">Workbench</SectionLabel>
379378
</Reveal>
380379
<div className="workbench-grid">
381-
{FEATURED_PROJECTS.map((project, index) => (
382-
<Reveal key={project.slug} delay={index * 70}>
380+
{pinnedRepos.map((project, index) => (
381+
<Reveal key={project.title} delay={index * 70}>
383382
<article className="workbench-card">
384383
<div className="card-head">
385384
<span style={{ backgroundColor: project.langColor }} aria-hidden="true" />
386385
<p>{projectLane(project)}</p>
387386
</div>
388-
<h3>{project.name}</h3>
389-
<p>{project.description}</p>
390-
<div className="project-tags">{project.tags.map((tag) => <span key={tag}>{tag}</span>)}</div>
387+
<h3>{project.title}</h3>
388+
<p>{project.description || "Repository note pending."}</p>
389+
<div className="project-tags">
390+
{project.stars > 0 && <span>{project.stars} stars</span>}
391+
{project.lang && <span>{project.lang}</span>}
392+
<span>Pinned on GitHub</span>
393+
</div>
391394
<div className="project-actions">
392-
<a href={project.repoUrl} target="_blank" rel="noreferrer noopener">Source</a>
393-
<a href={project.liveUrl} target="_blank" rel="noreferrer noopener">Live</a>
395+
<a href={project.url} target="_blank" rel="noreferrer noopener">Source</a>
396+
{project.pagesUrl && <a href={project.pagesUrl} target="_blank" rel="noreferrer noopener">Live</a>}
397+
{project.homepageUrl && project.homepageUrl !== project.pagesUrl && <a href={project.homepageUrl} target="_blank" rel="noreferrer noopener">Homepage</a>}
394398
</div>
395399
</article>
396400
</Reveal>
@@ -400,12 +404,12 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
400404
<Reveal delay={320}>
401405
<div className="repo-ledger-heading">
402406
<p className="font-mono text-[10px] uppercase tracking-[0.28em] text-dim">public repository ledger</p>
403-
<h3>All non-fork GitHub work</h3>
404-
<p>Fetched at build time from GitHub, excluding this portfolio, profile metadata, collaborator repos, and forks. Pages links resolve from each repository's live GitHub Pages site.</p>
407+
<h3>Other GitHub repos</h3>
408+
<p>Fetched at build time from GitHub, excluding pinned Workbench repos, this portfolio, profile metadata, collaborator repos, and forks. Pages links resolve from each repository's live GitHub Pages site.</p>
405409
</div>
406410
</Reveal>
407411
<div className="repo-table" role="list">
408-
{workbenchRepos.map((project, index) => (
412+
{otherRepos.map((project, index) => (
409413
<Reveal key={project.title} delay={index * 35}>
410414
<article role="listitem" className="repo-row">
411415
<a href={project.url} target="_blank" rel="noreferrer noopener">{project.title}</a>
@@ -420,7 +424,7 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
420424
</Reveal>
421425
))}
422426
</div>
423-
<Reveal delay={workbenchRepos.length * 35 + 120}>
427+
<Reveal delay={otherRepos.length * 35 + 120}>
424428
<a href="https://github.com/jonathanperis" target="_blank" rel="noreferrer noopener" className="github-tail">View all repositories on GitHub</a>
425429
</Reveal>
426430
</section>

src/lib/github.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,39 @@ export type GitHubRepo = {
88
homepageUrl?: string;
99
pagesUrl?: string;
1010
updatedAt?: string;
11+
pinned?: boolean;
1112
};
1213

1314
const GITHUB_OWNER = "jonathanperis";
1415
const EXCLUDE_REPOS = new Set(["jonathanperis.github.io", ".github", "jonathanperis"]);
1516

1617
const FALLBACK: GitHubRepo[] = [
17-
{ title: "cpnucleo", description: "Modern .NET sample — clean architecture, testing, DI, and Docker containerization.", url: "https://github.com/jonathanperis/cpnucleo", lang: "C#", langColor: "#178600", stars: 8, homepageUrl: "https://jonathanperis.github.io/cpnucleo/", pagesUrl: "https://jonathanperis.github.io/cpnucleo/" },
18-
{ title: "super-mango-editor", description: "A classic side-scrolling platformer built with C and SDL2 — playable in the browser via WebAssembly.", url: "https://github.com/jonathanperis/super-mango-editor", lang: "C", langColor: "#555555", stars: 0, homepageUrl: "https://jonathanperis.github.io/super-mango-editor/", pagesUrl: "https://jonathanperis.github.io/super-mango-editor/" },
19-
{ title: "rinha4-back-end-dotnet", description: "Rinha de Backend 2025 implementation in .NET with docs and benchmark reports.", url: "https://github.com/jonathanperis/rinha4-back-end-dotnet", lang: "C#", langColor: "#178600", stars: 0, homepageUrl: "https://jonathanperis.github.io/rinha4-back-end-dotnet/", pagesUrl: "https://jonathanperis.github.io/rinha4-back-end-dotnet/" },
20-
{ title: "rinha2-back-end-dotnet", description: "High-performance Rinha de Backend challenge in C# with PostgreSQL and Nginx.", url: "https://github.com/jonathanperis/rinha2-back-end-dotnet", lang: "C#", langColor: "#178600", stars: 3, homepageUrl: "https://jonathanperis.github.io/rinha2-back-end-dotnet/", pagesUrl: "https://jonathanperis.github.io/rinha2-back-end-dotnet/" },
21-
{ title: "rinha2-back-end-k6", description: "K6 load testing suite for the Rinha de Backend challenge.", url: "https://github.com/jonathanperis/rinha2-back-end-k6", lang: "JavaScript", langColor: "#f1e05a", stars: 0, homepageUrl: "https://jonathanperis.github.io/rinha2-back-end-k6/", pagesUrl: "https://jonathanperis.github.io/rinha2-back-end-k6/" },
22-
{ title: "blazor-mudblazor-starter", description: "Blazor + MudBlazor starter template with pre-configured components.", url: "https://github.com/jonathanperis/blazor-mudblazor-starter", lang: "HTML", langColor: "#e34c26", stars: 1, homepageUrl: "https://jonathanperis.github.io/blazor-mudblazor-starter/", pagesUrl: "https://jonathanperis.github.io/blazor-mudblazor-starter/" },
18+
{ title: "cpnucleo", description: "Modern .NET sample — clean architecture, testing, DI, and Docker containerization.", url: "https://github.com/jonathanperis/cpnucleo", lang: "C#", langColor: "#178600", stars: 8, homepageUrl: "https://jonathanperis.github.io/cpnucleo/", pagesUrl: "https://jonathanperis.github.io/cpnucleo/", pinned: true },
19+
{ title: "super-mango-editor", description: "A classic side-scrolling platformer built with C and SDL2 — playable in the browser via WebAssembly.", url: "https://github.com/jonathanperis/super-mango-editor", lang: "C", langColor: "#555555", stars: 0, homepageUrl: "https://jonathanperis.github.io/super-mango-editor/", pagesUrl: "https://jonathanperis.github.io/super-mango-editor/", pinned: true },
20+
{ title: "rinha2-back-end-dotnet", description: "High-performance Rinha de Backend challenge in C# with PostgreSQL and Nginx.", url: "https://github.com/jonathanperis/rinha2-back-end-dotnet", lang: "C#", langColor: "#178600", stars: 3, homepageUrl: "https://jonathanperis.github.io/rinha2-back-end-dotnet/", pagesUrl: "https://jonathanperis.github.io/rinha2-back-end-dotnet/", pinned: true },
21+
{ title: "rinha2-back-end-go", description: "Rinha de Backend in Go — high-performance with PostgreSQL and Nginx.", url: "https://github.com/jonathanperis/rinha2-back-end-go", lang: "PLpgSQL", langColor: "#336790", stars: 1, homepageUrl: "https://jonathanperis.github.io/rinha2-back-end-go/", pagesUrl: "https://jonathanperis.github.io/rinha2-back-end-go/", pinned: true },
22+
{ title: "blazor-mudblazor-starter", description: "Blazor + MudBlazor starter template with pre-configured components.", url: "https://github.com/jonathanperis/blazor-mudblazor-starter", lang: "HTML", langColor: "#e34c26", stars: 1, homepageUrl: "https://jonathanperis.github.io/blazor-mudblazor-starter/", pagesUrl: "https://jonathanperis.github.io/blazor-mudblazor-starter/", pinned: true },
23+
{ title: "speedy-bird-lynx", description: "Lynx motion/game experiment in C++.", url: "https://github.com/jonathanperis/speedy-bird-lynx", lang: "C++", langColor: "#f34b7d", stars: 0, homepageUrl: "https://jonathanperis.github.io/speedy-bird-lynx/", pagesUrl: "https://jonathanperis.github.io/speedy-bird-lynx/", pinned: true },
2324
{ title: "rinha4-back-end-c", description: "Rinha de Backend 2025 C implementation with GitHub Pages documentation.", url: "https://github.com/jonathanperis/rinha4-back-end-c", lang: "C", langColor: "#555555", stars: 0, homepageUrl: "https://jonathanperis.github.io/rinha4-back-end-c/", pagesUrl: "https://jonathanperis.github.io/rinha4-back-end-c/" },
24-
{ title: "rinha2-back-end-go", description: "Rinha de Backend in Go — high-performance with PostgreSQL and Nginx.", url: "https://github.com/jonathanperis/rinha2-back-end-go", lang: "PLpgSQL", langColor: "#336790", stars: 1, homepageUrl: "https://jonathanperis.github.io/rinha2-back-end-go/", pagesUrl: "https://jonathanperis.github.io/rinha2-back-end-go/" },
25+
{ title: "rinha2-back-end-k6", description: "K6 load testing suite for the Rinha de Backend challenge.", url: "https://github.com/jonathanperis/rinha2-back-end-k6", lang: "JavaScript", langColor: "#f1e05a", stars: 0, homepageUrl: "https://jonathanperis.github.io/rinha2-back-end-k6/", pagesUrl: "https://jonathanperis.github.io/rinha2-back-end-k6/" },
2526
];
2627

2728
const QUERY = `{
2829
user(login: "${GITHUB_OWNER}") {
30+
pinnedItems(first: 100, types: REPOSITORY) {
31+
nodes {
32+
... on Repository {
33+
name
34+
description
35+
url
36+
homepageUrl
37+
stargazerCount
38+
updatedAt
39+
owner { login }
40+
primaryLanguage { name color }
41+
}
42+
}
43+
}
2944
repositories(first: 100, privacy: PUBLIC, orderBy: { field: UPDATED_AT, direction: DESC }, isFork: false) {
3045
nodes {
3146
name
@@ -84,7 +99,7 @@ async function fetchPagesUrl(repoName: string, token: string): Promise<string |
8499
}
85100
}
86101

87-
function normalizeRepo(n: RepoNode): GitHubRepo {
102+
function normalizeRepo(n: RepoNode, pinned = false): GitHubRepo {
88103
const homepageUrl = n.homepageUrl?.trim() || undefined;
89104

90105
return {
@@ -97,6 +112,7 @@ function normalizeRepo(n: RepoNode): GitHubRepo {
97112
homepageUrl,
98113
pagesUrl: homepageUrl?.startsWith(`https://${GITHUB_OWNER}.github.io/`) ? homepageUrl : undefined,
99114
updatedAt: n.updatedAt,
115+
pinned,
100116
};
101117
}
102118

@@ -123,16 +139,29 @@ export async function fetchRepos(): Promise<GitHubRepo[]> {
123139
}
124140

125141
const json = await res.json();
142+
const pinnedNodes = json?.data?.user?.pinnedItems?.nodes;
126143
const nodes = json?.data?.user?.repositories?.nodes;
127144

128145
if (!Array.isArray(nodes) || nodes.length === 0) {
129146
console.error("[github] No repos found in response");
130147
return FALLBACK;
131148
}
132149

150+
const pinnedOrder = new Map<string, number>(
151+
Array.isArray(pinnedNodes)
152+
? pinnedNodes
153+
.filter((n: RepoNode) => n.owner.login === GITHUB_OWNER && !EXCLUDE_REPOS.has(n.name))
154+
.map((n: RepoNode, index: number) => [n.name, index])
155+
: [],
156+
);
157+
133158
const repos = nodes
134159
.filter((n: RepoNode) => n.owner.login === GITHUB_OWNER && !EXCLUDE_REPOS.has(n.name))
135-
.map(normalizeRepo);
160+
.map((n: RepoNode) => normalizeRepo(n, pinnedOrder.has(n.name)))
161+
.sort((a, b) => {
162+
if (a.pinned && b.pinned) return (pinnedOrder.get(a.title) ?? 0) - (pinnedOrder.get(b.title) ?? 0);
163+
return Number(b.pinned) - Number(a.pinned);
164+
});
136165

137166
const pagesUrls = await Promise.all(repos.map((repo) => fetchPagesUrl(repo.title, token)));
138167

wiki/dynamic_projects.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@
22

33
## How It Works
44

5-
The Workbench repository ledger is dynamic. During the Astro build, `src/lib/github.ts` fetches Jonathan's owned public, non-fork repositories from GitHub, excludes profile/portfolio metadata repos, and enriches each row with its live GitHub Pages URL when Pages is enabled.
5+
The Workbench repository data is dynamic. During the Astro build, `src/lib/github.ts` fetches Jonathan's GitHub profile pinned repositories plus owned public, non-fork repositories from GitHub, excludes profile/portfolio metadata repos, and enriches each entry with its live GitHub Pages URL when Pages is enabled.
66

7-
Featured project cards remain curated in `src/lib/data.ts` so the top of the Workbench can emphasize the strongest portfolio examples. The dynamic ledger lists the remaining repositories below those cards.
7+
The major Workbench cards are the repositories currently pinned on the `jonathanperis` GitHub profile. The ledger below them is labeled "Other GitHub repos" and lists the remaining owned public, non-fork repositories.
88

99
## Data Flow
1010

1111
1. `src/pages/index.astro` calls `fetchRepos()` from `src/lib/github.ts`.
12-
2. `github.ts` sends a GraphQL query to GitHub for public repositories ordered by recent update:
12+
2. `github.ts` sends a GraphQL query to GitHub for pinned repository items and public repositories ordered by recent update:
1313

1414
```graphql
1515
{
1616
user(login: "jonathanperis") {
17+
pinnedItems(first: 100, types: REPOSITORY) {
18+
nodes {
19+
... on Repository {
20+
name
21+
}
22+
}
23+
}
1724
repositories(first: 100, privacy: PUBLIC, orderBy: { field: UPDATED_AT, direction: DESC }, isFork: false) {
1825
nodes {
1926
name
@@ -29,9 +36,10 @@ Featured project cards remain curated in `src/lib/data.ts` so the top of the Wor
2936
}
3037
```
3138

32-
3. For each included repository, `github.ts` also checks the REST Pages endpoint: `GET /repos/jonathanperis/{repo}/pages`.
33-
4. The response is mapped to `GitHubRepo[]` and passed to the React `Portfolio` component.
34-
5. At build time, this data is baked into the static HTML.
39+
3. The mapper marks repos that appear in `pinnedItems` as `pinned: true` and preserves the GitHub profile pinned order for the major cards.
40+
4. For each included repository, `github.ts` also checks the REST Pages endpoint: `GET /repos/jonathanperis/{repo}/pages`.
41+
5. The response is mapped to `GitHubRepo[]` and passed to the React `Portfolio` component.
42+
6. At build time, this data is baked into the static HTML.
3543

3644
## Filtering
3745

@@ -43,7 +51,7 @@ The code also excludes repositories that should not appear in the public Workben
4351
- `.github` — organization/profile metadata
4452
- `jonathanperis` — profile/readme metadata
4553

46-
Featured project slugs from `FEATURED_PROJECTS` are removed from the ledger so they are not duplicated below the curated cards.
54+
Pinned repositories are removed from the "Other GitHub repos" ledger so they are not duplicated below the major Workbench cards.
4755

4856
## GitHub Pages Links
4957

wiki/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Personal developer portfolio for **Jonathan Peris** — Software Engineer with 1
88

99
- Developer-themed dark UI with terminal aesthetic
1010
- Typing role animation, scroll animations, progress bar
11-
- Dynamic Workbench repository ledger fetched at build time via GitHub GraphQL + Pages REST APIs
11+
- Dynamic Workbench pinned-repo cards and "Other GitHub repos" ledger fetched at build time via GitHub GraphQL + Pages REST APIs
1212
- Print-optimized resume page generated from shared data (`/resume`)
1313
- Interactive terminal easter egg (Konami code)
1414
- SEO optimized: JSON-LD, sitemap, robots.txt, Open Graph, Twitter cards

0 commit comments

Comments
 (0)