Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down
40 changes: 22 additions & 18 deletions src/components/Portfolio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
AVAILABILITY,
ENGINEERING_PRINCIPLES,
EXPERIENCES,
FEATURED_PROJECTS,
OPERATING_SIGNALS,
PROFILE,
SKILLS,
Expand Down Expand Up @@ -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("");
Expand Down Expand Up @@ -378,19 +377,24 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
<SectionLabel id="workbench-heading" number="04">Workbench</SectionLabel>
</Reveal>
<div className="workbench-grid">
{FEATURED_PROJECTS.map((project, index) => (
<Reveal key={project.slug} delay={index * 70}>
{pinnedRepos.map((project, index) => (
<Reveal key={project.title} delay={index * 70}>
<article className="workbench-card">
<div className="card-head">
<span style={{ backgroundColor: project.langColor }} aria-hidden="true" />
<p>{projectLane(project)}</p>
</div>
<h3>{project.name}</h3>
<p>{project.description}</p>
<div className="project-tags">{project.tags.map((tag) => <span key={tag}>{tag}</span>)}</div>
<h3>{project.title}</h3>
<p>{project.description || "Repository note pending."}</p>
<div className="project-tags">
{project.stars > 0 && <span>{project.stars} stars</span>}
{project.lang && <span>{project.lang}</span>}
<span>Pinned on GitHub</span>
</div>
<div className="project-actions">
<a href={project.repoUrl} target="_blank" rel="noreferrer noopener">Source</a>
<a href={project.liveUrl} target="_blank" rel="noreferrer noopener">Live</a>
<a href={project.url} target="_blank" rel="noreferrer noopener">Source</a>
{project.pagesUrl && <a href={project.pagesUrl} target="_blank" rel="noreferrer noopener">Live</a>}
{project.homepageUrl && project.homepageUrl !== project.pagesUrl && <a href={project.homepageUrl} target="_blank" rel="noreferrer noopener">Homepage</a>}
</div>
</article>
</Reveal>
Expand All @@ -400,12 +404,12 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
<Reveal delay={320}>
<div className="repo-ledger-heading">
<p className="font-mono text-[10px] uppercase tracking-[0.28em] text-dim">public repository ledger</p>
<h3>All non-fork GitHub work</h3>
<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>
<h3>Other GitHub repos</h3>
<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>
</div>
</Reveal>
<div className="repo-table" role="list">
{workbenchRepos.map((project, index) => (
{otherRepos.map((project, index) => (
<Reveal key={project.title} delay={index * 35}>
<article role="listitem" className="repo-row">
<a href={project.url} target="_blank" rel="noreferrer noopener">{project.title}</a>
Expand All @@ -420,7 +424,7 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
</Reveal>
))}
</div>
<Reveal delay={workbenchRepos.length * 35 + 120}>
<Reveal delay={otherRepos.length * 35 + 120}>
<a href="https://github.com/jonathanperis" target="_blank" rel="noreferrer noopener" className="github-tail">View all repositories on GitHub</a>
</Reveal>
</section>
Expand Down
47 changes: 38 additions & 9 deletions src/lib/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -84,7 +99,7 @@ async function fetchPagesUrl(repoName: string, token: string): Promise<string |
}
}

function normalizeRepo(n: RepoNode): GitHubRepo {
function normalizeRepo(n: RepoNode, pinned = false): GitHubRepo {
const homepageUrl = n.homepageUrl?.trim() || undefined;

return {
Expand All @@ -97,6 +112,7 @@ function normalizeRepo(n: RepoNode): GitHubRepo {
homepageUrl,
pagesUrl: homepageUrl?.startsWith(`https://${GITHUB_OWNER}.github.io/`) ? homepageUrl : undefined,
updatedAt: n.updatedAt,
pinned,
};
}

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

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) {
console.error("[github] No repos found in response");
return FALLBACK;
}

const pinnedOrder = new Map<string, number>(
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)));

Expand Down
22 changes: 15 additions & 7 deletions wiki/dynamic_projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion wiki/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down