From a15b52bdf21ffdc450a508e29c81aa3f51da79e5 Mon Sep 17 00:00:00 2001 From: Jonathan Peris Date: Sun, 17 May 2026 00:52:48 +0000 Subject: [PATCH] feat: source workbench cards from pinned repos --- README.md | 5 ++-- src/components/Portfolio.tsx | 40 ++++++++++++++++-------------- src/lib/github.ts | 47 +++++++++++++++++++++++++++++------- wiki/dynamic_projects.md | 22 +++++++++++------ wiki/index.md | 2 +- 5 files changed, 79 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 13055ee..3fb9a0e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## About -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. +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. 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. @@ -29,7 +29,8 @@ It is built to stay simple to deploy: build locally, export statically, and publ ## Features -- Dynamic Workbench repository ledger from GitHub GraphQL API (public, non-fork repos) +- Workbench major cards sourced from GitHub profile pinned repositories +- Dynamic "Other GitHub repos" ledger from GitHub GraphQL API (public, non-fork repos) - Live GitHub Pages links resolved at build time via GitHub REST API - Terminal-themed dark UI with typing animations and scroll effects - Print-optimized resume page with download support diff --git a/src/components/Portfolio.tsx b/src/components/Portfolio.tsx index ea0c45b..3758646 100644 --- a/src/components/Portfolio.tsx +++ b/src/components/Portfolio.tsx @@ -5,7 +5,6 @@ import { AVAILABILITY, ENGINEERING_PRINCIPLES, EXPERIENCES, - FEATURED_PROJECTS, OPERATING_SIGNALS, PROFILE, SKILLS, @@ -151,18 +150,18 @@ const SKILL_GROUPS: Array<{ key: keyof typeof SKILLS; label: string; path: strin { key: "frontend", label: "interface", path: "/stack/interface" }, ]; -function projectLane(project: (typeof FEATURED_PROJECTS)[number]) { - const text = `${project.name} ${project.tags.join(" ")}`.toLowerCase(); +function projectLane(project: GitHubRepo) { + const text = `${project.title} ${project.description} ${project.lang}`.toLowerCase(); if (text.includes("rinha") || text.includes("k6") || text.includes("performance")) return "load path"; - if (text.includes("clean") || text.includes(".net")) return "system design"; - if (text.includes("game") || text.includes("sdl") || text.includes("lynx")) return "runtime lab"; + if (text.includes("clean") || text.includes(".net") || text.includes("blazor")) return "system design"; + if (text.includes("game") || text.includes("sdl") || text.includes("lynx") || text.includes("mango")) return "runtime lab"; return "field note"; } export default function Portfolio({ projects }: { projects: GitHubRepo[] }) { const scrollProgress = useScrollProgress(); - const featuredSlugs = useMemo(() => new Set(FEATURED_PROJECTS.map((fp) => fp.slug)), []); - const workbenchRepos = useMemo(() => projects.filter((project) => !featuredSlugs.has(project.title)), [featuredSlugs, projects]); + const pinnedRepos = useMemo(() => projects.filter((project) => project.pinned), [projects]); + const otherRepos = useMemo(() => projects.filter((project) => !project.pinned), [projects]); const [termOpen, setTermOpen] = useState(false); const [termInput, setTermInput] = useState(""); @@ -378,19 +377,24 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) { Workbench
- {FEATURED_PROJECTS.map((project, index) => ( - + {pinnedRepos.map((project, index) => ( +
-

{project.name}

-

{project.description}

-
{project.tags.map((tag) => {tag})}
+

{project.title}

+

{project.description || "Repository note pending."}

+
+ {project.stars > 0 && {project.stars} stars} + {project.lang && {project.lang}} + Pinned on GitHub +
- Source - Live + Source + {project.pagesUrl && Live} + {project.homepageUrl && project.homepageUrl !== project.pagesUrl && Homepage}
@@ -400,12 +404,12 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {

public repository ledger

-

All non-fork GitHub work

-

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.

+

Other GitHub repos

+

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.

- {workbenchRepos.map((project, index) => ( + {otherRepos.map((project, index) => (
{project.title} @@ -420,7 +424,7 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) { ))}
- + View all repositories on GitHub diff --git a/src/lib/github.ts b/src/lib/github.ts index 8c9279c..0bb3614 100644 --- a/src/lib/github.ts +++ b/src/lib/github.ts @@ -8,24 +8,39 @@ export type GitHubRepo = { homepageUrl?: string; pagesUrl?: string; updatedAt?: string; + pinned?: boolean; }; const GITHUB_OWNER = "jonathanperis"; const EXCLUDE_REPOS = new Set(["jonathanperis.github.io", ".github", "jonathanperis"]); const FALLBACK: GitHubRepo[] = [ - { 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/" }, - { 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/" }, - { 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/" }, - { 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/" }, - { 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/" }, - { 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/" }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, + { 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 }, { 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/" }, - { 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/" }, + { 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/" }, ]; const QUERY = `{ user(login: "${GITHUB_OWNER}") { + pinnedItems(first: 100, types: REPOSITORY) { + nodes { + ... on Repository { + name + description + url + homepageUrl + stargazerCount + updatedAt + owner { login } + primaryLanguage { name color } + } + } + } repositories(first: 100, privacy: PUBLIC, orderBy: { field: UPDATED_AT, direction: DESC }, isFork: false) { nodes { name @@ -84,7 +99,7 @@ async function fetchPagesUrl(repoName: string, token: string): Promise { } const json = await res.json(); + const pinnedNodes = json?.data?.user?.pinnedItems?.nodes; const nodes = json?.data?.user?.repositories?.nodes; if (!Array.isArray(nodes) || nodes.length === 0) { @@ -130,9 +147,21 @@ export async function fetchRepos(): Promise { return FALLBACK; } + const pinnedOrder = new Map( + Array.isArray(pinnedNodes) + ? pinnedNodes + .filter((n: RepoNode) => n.owner.login === GITHUB_OWNER && !EXCLUDE_REPOS.has(n.name)) + .map((n: RepoNode, index: number) => [n.name, index]) + : [], + ); + const repos = nodes .filter((n: RepoNode) => n.owner.login === GITHUB_OWNER && !EXCLUDE_REPOS.has(n.name)) - .map(normalizeRepo); + .map((n: RepoNode) => normalizeRepo(n, pinnedOrder.has(n.name))) + .sort((a, b) => { + if (a.pinned && b.pinned) return (pinnedOrder.get(a.title) ?? 0) - (pinnedOrder.get(b.title) ?? 0); + return Number(b.pinned) - Number(a.pinned); + }); const pagesUrls = await Promise.all(repos.map((repo) => fetchPagesUrl(repo.title, token))); diff --git a/wiki/dynamic_projects.md b/wiki/dynamic_projects.md index aa8a52d..0993bf5 100644 --- a/wiki/dynamic_projects.md +++ b/wiki/dynamic_projects.md @@ -2,18 +2,25 @@ ## How It Works -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. +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. -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. +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. ## Data Flow 1. `src/pages/index.astro` calls `fetchRepos()` from `src/lib/github.ts`. -2. `github.ts` sends a GraphQL query to GitHub for public repositories ordered by recent update: +2. `github.ts` sends a GraphQL query to GitHub for pinned repository items and public repositories ordered by recent update: ```graphql { user(login: "jonathanperis") { + pinnedItems(first: 100, types: REPOSITORY) { + nodes { + ... on Repository { + name + } + } + } repositories(first: 100, privacy: PUBLIC, orderBy: { field: UPDATED_AT, direction: DESC }, isFork: false) { nodes { name @@ -29,9 +36,10 @@ Featured project cards remain curated in `src/lib/data.ts` so the top of the Wor } ``` -3. For each included repository, `github.ts` also checks the REST Pages endpoint: `GET /repos/jonathanperis/{repo}/pages`. -4. The response is mapped to `GitHubRepo[]` and passed to the React `Portfolio` component. -5. At build time, this data is baked into the static HTML. +3. The mapper marks repos that appear in `pinnedItems` as `pinned: true` and preserves the GitHub profile pinned order for the major cards. +4. For each included repository, `github.ts` also checks the REST Pages endpoint: `GET /repos/jonathanperis/{repo}/pages`. +5. The response is mapped to `GitHubRepo[]` and passed to the React `Portfolio` component. +6. At build time, this data is baked into the static HTML. ## Filtering @@ -43,7 +51,7 @@ The code also excludes repositories that should not appear in the public Workben - `.github` — organization/profile metadata - `jonathanperis` — profile/readme metadata -Featured project slugs from `FEATURED_PROJECTS` are removed from the ledger so they are not duplicated below the curated cards. +Pinned repositories are removed from the "Other GitHub repos" ledger so they are not duplicated below the major Workbench cards. ## GitHub Pages Links diff --git a/wiki/index.md b/wiki/index.md index 028c475..2c30c11 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -8,7 +8,7 @@ Personal developer portfolio for **Jonathan Peris** — Software Engineer with 1 - Developer-themed dark UI with terminal aesthetic - Typing role animation, scroll animations, progress bar -- Dynamic Workbench repository ledger fetched at build time via GitHub GraphQL + Pages REST APIs +- Dynamic Workbench pinned-repo cards and "Other GitHub repos" ledger fetched at build time via GitHub GraphQL + Pages REST APIs - Print-optimized resume page generated from shared data (`/resume`) - Interactive terminal easter egg (Konami code) - SEO optimized: JSON-LD, sitemap, robots.txt, Open Graph, Twitter cards