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
68 changes: 63 additions & 5 deletions src/components/Portfolio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,33 @@ const SKILL_GROUPS: Array<{ key: keyof typeof SKILLS; label: string; path: strin
{ key: "frontend", label: "interface", path: "/stack/interface" },
];


const EXPERIMENTS = {
control: {
label: "A/control",
lede: "I build backend systems that can be understood, operated, and changed after they meet production traffic.",
primary: { label: "View resume", href: "/resume", event: "hero_resume" },
secondary: { label: "Contact on LinkedIn", href: "https://www.linkedin.com/in/jonathan-peris/", event: "hero_linkedin" },
tertiary: { label: "See projects", href: "#workbench", event: "hero_projects" },
},
clarity: {
label: "B/clarity",
lede: "I design and ship reliable .NET platforms for fintech, cloud, and high-change business domains.",
primary: { label: "Contact me", href: "mailto:jperis.silva@gmail.com", event: "hero_email" },
secondary: { label: "View resume", href: "/resume", event: "hero_resume" },
tertiary: { label: "See projects", href: "#workbench", event: "hero_projects" },
},
workbench: {
label: "C/workbench",
lede: "Architecture, delivery, and performance workbench for production systems that need clear ownership.",
primary: { label: "Open workbench", href: "#workbench", event: "hero_workbench" },
secondary: { label: "Contact me", href: "mailto:jperis.silva@gmail.com", event: "hero_email" },
tertiary: { label: "Resume", href: "/resume", event: "hero_resume" },
},
} as const;

type ExperimentKey = keyof typeof EXPERIMENTS;

function projectLane(project: (typeof FEATURED_PROJECTS)[number]) {
const text = `${project.name} ${project.tags.join(" ")}`.toLowerCase();
if (text.includes("rinha") || text.includes("k6") || text.includes("performance")) return "load path";
Expand All @@ -162,6 +189,7 @@ function projectLane(project: (typeof FEATURED_PROJECTS)[number]) {
export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
const scrollProgress = useScrollProgress();
const featuredSlugs = useMemo(() => new Set(FEATURED_PROJECTS.map((fp) => fp.slug)), []);
const [experiment, setExperiment] = useState<ExperimentKey>("control");

const [termOpen, setTermOpen] = useState(false);
const [termInput, setTermInput] = useState("");
Expand All @@ -174,6 +202,23 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
const lastFocusedRef = useRef<HTMLElement | null>(null);
const konamiRef = useRef<string[]>([]);

useEffect(() => {
const keys = Object.keys(EXPERIMENTS) as ExperimentKey[];
const params = new URLSearchParams(window.location.search);
const requested = params.get("ab") as ExperimentKey | null;
const stored = window.localStorage.getItem("jp.heroExperiment") as ExperimentKey | null;
const selected = requested && keys.includes(requested)
? requested
: stored && keys.includes(stored)
? stored
: keys[Math.floor(Math.random() * keys.length)];

window.localStorage.setItem("jp.heroExperiment", selected);
setExperiment(selected);
document.documentElement.dataset.heroExperiment = selected;
trackEvent("experiment_view", { experiment: "hero_conversion", variant: selected });
}, []);

useEffect(() => {
const K = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"];
const onKey = (e: globalThis.KeyboardEvent) => {
Expand Down Expand Up @@ -202,6 +247,12 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
requestAnimationFrame(() => lastFocusedRef.current?.focus());
}, []);

const openTerminal = useCallback(() => {
lastFocusedRef.current = document.activeElement as HTMLElement | null;
setTermOpen(true);
trackEvent("terminal_open", { source: "visible_hint" });
}, []);

const submit = useCallback(() => {
if (!termInput.trim()) return;
const result = runCmd(termInput);
Expand Down Expand Up @@ -247,6 +298,8 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
[closeTerminal, cmdHist, histIdx, submit],
);

const activeExperiment = EXPERIMENTS[experiment];

return (
<>
<div className="scroll-bar fixed top-0 left-0 right-0 h-[2px] z-50" style={{ transform: `scaleX(${scrollProgress})` }} />
Expand Down Expand Up @@ -274,11 +327,14 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
<h1 id="hero-title">Jonathan Peris</h1>
<p className="role-line"><span>Backend architecture / .NET / Azure</span><span className="typing-cursor" aria-hidden="true" /></p>
<p className="hero-lede">
I build backend systems that can be understood, operated, and changed after they meet production traffic.
{activeExperiment.lede}
</p>
<p className="experiment-badge" aria-label={`Active hero experiment ${activeExperiment.label}`}>{activeExperiment.label}</p>
<div className="hero-actions">
<a href="/resume" className="primary-action" onClick={() => trackEvent("cta_click", { label: "hero_resume" })}>View resume</a>
<a href="https://www.linkedin.com/in/jonathan-peris/" target="_blank" rel="noreferrer noopener" className="secondary-action" onClick={() => trackEvent("cta_click", { label: "hero_linkedin" })}>Contact on LinkedIn</a>
<a href={activeExperiment.primary.href} className="primary-action" onClick={() => trackEvent("cta_click", { label: activeExperiment.primary.event, variant: experiment })}>{activeExperiment.primary.label}</a>
<a href={activeExperiment.secondary.href} target={activeExperiment.secondary.href.startsWith("http") ? "_blank" : undefined} rel={activeExperiment.secondary.href.startsWith("http") ? "noreferrer noopener" : undefined} className="secondary-action" onClick={() => trackEvent("cta_click", { label: activeExperiment.secondary.event, variant: experiment })}>{activeExperiment.secondary.label}</a>
<a href={activeExperiment.tertiary.href} className="secondary-action" onClick={() => trackEvent("cta_click", { label: activeExperiment.tertiary.event, variant: experiment })}>{activeExperiment.tertiary.label}</a>
<button type="button" className="ghost-action" onClick={openTerminal}>open ~/terminal</button>
</div>
<div className="signal-strip" aria-label="Operating signals">
{OPERATING_SIGNALS.map((signal) => (
Expand Down Expand Up @@ -386,10 +442,11 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
</div>
<h3>{project.name}</h3>
<p>{project.description}</p>
<p className="project-proof">{project.proof}</p>
<div className="project-tags">{project.tags.map((tag) => <span key={tag}>{tag}</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.liveUrl} target="_blank" rel="noreferrer noopener" aria-label={`${project.liveLabel} for ${project.name}`} onClick={() => trackEvent("project_click", { project: project.slug, target: "live" })}>{project.liveLabel}</a>
<a href={project.repoUrl} target="_blank" rel="noreferrer noopener" aria-label={`${project.repoLabel} for ${project.name}`} onClick={() => trackEvent("project_click", { project: project.slug, target: "source" })}>{project.repoLabel}</a>
</div>
</article>
</Reveal>
Expand Down Expand Up @@ -427,6 +484,7 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
{SOCIALS.map((social) => <SocialLink key={social.label} social={social} compact />)}
</div>
<p>Built as a small systems manual. Hidden shell: ↑↑↓↓←→←→BA</p>
<button type="button" className="footer-terminal" onClick={openTerminal}>open ~/terminal</button>
</footer>
</div>

Expand Down
58 changes: 41 additions & 17 deletions src/lib/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ export const PROFILE = {
};

export const AVAILABILITY = {
short: "Open to remote roles + consulting",
full: "Open to remote roles and select backend architecture consulting.",
short: "Available for remote backend, architecture & consulting work",
full: "Available for remote backend roles, architecture reviews, and select consulting work.",
};

export const OPERATING_SIGNALS = [
{ label: "12+ yrs", value: "production software" },
{ label: ".NET + Azure", value: "primary lane" },
{ label: "Fintech", value: "domain depth" },
{ label: "Remote", value: "Brazil to US teams" },
{ label: "Systems", value: "architecture + delivery" },
];
Expand Down Expand Up @@ -161,43 +162,58 @@ export type FeaturedProject = {
slug: string;
name: string;
description: string;
proof: string;
repoUrl: string;
liveUrl: string;
liveLabel: string;
repoLabel: string;
lang: string;
langColor: string;
tags: string[];
};

export const FEATURED_PROJECTS: FeaturedProject[] = [
{
slug: "speedy-bird-lynx",
name: "Speedy Bird",
description:
"A Flappy Bird clone built with Lynx (ReactLynx + TypeScript) — ByteDance's cross-platform native UI framework. One codebase renders natively on iOS, Android, and Web. Features accelerating difficulty, medal system, and a full CI/CD pipeline via GitHub Actions.",
repoUrl: "https://github.com/jonathanperis/speedy-bird-lynx",
liveUrl: "https://jonathanperis.github.io/speedy-bird-lynx/",
lang: "TypeScript",
langColor: "#3178c6",
tags: ["Lynx", "ReactLynx", "TypeScript", "Cross-Platform", "Game Dev"],
},
{
slug: "cpnucleo",
name: "Cpnucleo",
description:
"A full-featured .NET 10 reference implementation — Clean Architecture, DDD, dual REST/gRPC APIs, and 25+ architecture tests enforced at build time. Docs, architecture overview, and API reference available on GitHub Pages.",
"A production-shaped .NET 10 reference implementation for systems that need boundaries, tests, and delivery discipline instead of framework theater.",
proof:
"Clean Architecture, DDD, dual REST/gRPC APIs, Docker, DI, and 25+ architecture tests enforced at build time.",
repoUrl: "https://github.com/jonathanperis/cpnucleo",
liveUrl: "https://jonathanperis.github.io/cpnucleo/",
liveLabel: "View docs",
repoLabel: "View source",
lang: "C#",
langColor: "#178600",
tags: ["Clean Architecture", ".NET", "Docker", "DI", "Testing"],
},
{
slug: "speedy-bird-lynx",
name: "Speedy Bird",
description:
"A Flappy Bird clone built with Lynx (ReactLynx + TypeScript), proving a single codebase can render natively across mobile and web surfaces.",
proof:
"Includes accelerating difficulty, medal scoring, web deployment, and a GitHub Actions delivery path for repeatable demos.",
repoUrl: "https://github.com/jonathanperis/speedy-bird-lynx",
liveUrl: "https://jonathanperis.github.io/speedy-bird-lynx/",
liveLabel: "Play demo",
repoLabel: "View source",
lang: "TypeScript",
langColor: "#3178c6",
tags: ["Lynx", "ReactLynx", "TypeScript", "Cross-Platform", "Game Dev"],
},
{
slug: "super-mango-editor",
name: "Super Mango Editor",
description:
"A classic side-scrolling platformer built from scratch with C and SDL2. Compiled to WebAssembly so it runs directly in the browser — no install needed. Features sprite animation, collision detection, and retro-style gameplay.",
"A classic side-scrolling platformer built from scratch with C and SDL2, then compiled to WebAssembly for instant browser play.",
proof:
"Shows low-level runtime work: sprite animation, collision detection, Emscripten packaging, and retro gameplay constraints.",
repoUrl: "https://github.com/jonathanperis/super-mango-editor",
liveUrl: "https://jonathanperis.github.io/super-mango-editor/",
liveLabel: "Play in browser",
repoLabel: "View source",
lang: "C",
langColor: "#555555",
tags: ["C", "SDL2", "WebAssembly", "Game Dev", "Emscripten"],
Expand All @@ -206,9 +222,13 @@ export const FEATURED_PROJECTS: FeaturedProject[] = [
slug: "rinha2-back-end-dotnet",
name: "Rinha de Backend 2 — .NET",
description:
"My entry for the Rinha de Backend 2024/Q1 challenge — a high-performance concurrency-focused API built in C# with PostgreSQL and Nginx. Designed to handle extreme load under strict resource constraints (1.5 CPU / 550MB RAM).",
"A concurrency-focused API built for Rinha de Backend 2024/Q1, where the useful signal is correctness under pressure, not just happy-path latency.",
proof:
"C#, PostgreSQL, and Nginx under strict 1.5 CPU / 550MB RAM constraints with load-oriented architecture choices.",
repoUrl: "https://github.com/jonathanperis/rinha2-back-end-dotnet",
liveUrl: "https://jonathanperis.github.io/rinha2-back-end-dotnet/",
liveLabel: "View benchmark notes",
repoLabel: "View source",
lang: "C#",
langColor: "#178600",
tags: ["C#", "PostgreSQL", "Nginx", "High Performance", "Docker"],
Expand All @@ -217,9 +237,13 @@ export const FEATURED_PROJECTS: FeaturedProject[] = [
slug: "rinha2-back-end-k6",
name: "Rinha de Backend 2 — K6 Load Tests",
description:
"Load testing suite for the Rinha de Backend 2024/Q1 challenge using Grafana K6. Simulates realistic concurrent traffic patterns to stress-test API endpoints and validate correctness under heavy load.",
"A Grafana K6 load-testing suite for validating Rinha-style APIs against realistic concurrent traffic instead of hand-wavy performance claims.",
proof:
"Encodes stress scenarios, endpoint validation, and repeatable pressure tests that make backend behavior observable.",
repoUrl: "https://github.com/jonathanperis/rinha2-back-end-k6",
liveUrl: "https://jonathanperis.github.io/rinha2-back-end-k6/",
liveLabel: "Open load tests",
repoLabel: "View source",
lang: "JavaScript",
langColor: "#f1e05a",
tags: ["K6", "Load Testing", "Grafana", "Performance", "Stress Testing"],
Expand Down
35 changes: 32 additions & 3 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
--color-rose: oklch(72% 0.17 18);
--color-amber: oklch(78% 0.16 68);
--font-sans: "DM Sans", -apple-system, system-ui, sans-serif;
--font-display: "DM Sans", -apple-system, system-ui, sans-serif;
--font-display: "DM Serif Display", Georgia, serif;
--font-mono: "JetBrains Mono", "Fira Code", monospace;
}

Expand Down Expand Up @@ -163,8 +163,8 @@ body::after {
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
.code-block-accent {
border-left: 3px solid var(--color-violet);
box-shadow: -4px 0 20px rgba(74, 222, 128, 0.1);
border-color: oklch(76% 0.18 151 / 0.28);
box-shadow: inset 0 3px 0 var(--color-violet), 0 0 20px rgba(74, 222, 128, 0.1);
}
.code-block .titlebar {
background: var(--color-elevated);
Expand Down Expand Up @@ -464,6 +464,19 @@ body::after {
font-size: clamp(1.04rem, 2vw, 1.22rem);
line-height: 1.75;
}
.experiment-badge {
display: inline-flex;
margin-top: 1rem;
border: 1px solid oklch(76% 0.18 151 / 0.18);
border-radius: 999px;
background: oklch(94% 0.025 145 / 0.028);
color: var(--color-dim);
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.12em;
padding: 0.35rem 0.55rem;
text-transform: uppercase;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -727,8 +740,19 @@ body::after {
letter-spacing: 0.16em;
}
.workbench-card > p { margin: 0.7rem 0 1rem; }
.workbench-card .project-proof {
border-top: 1px solid oklch(94% 0.025 145 / 0.08);
border-bottom: 1px solid oklch(94% 0.025 145 / 0.08);
margin: 1rem 0;
padding: 0.85rem 0;
color: var(--color-text);
font-family: var(--font-mono);
font-size: 0.76rem;
line-height: 1.65;
}
.project-actions {
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
margin-top: 1.1rem;
font-family: var(--font-mono);
Expand Down Expand Up @@ -796,6 +820,11 @@ body::after {
.icon-link:hover { color: var(--color-green); }
.icon-link svg { width: 1rem; height: 1rem; }
.icon-link.compact span { display: none; }
.footer-terminal {
color: var(--color-green);
cursor: pointer;
}
.footer-terminal:hover { color: var(--color-violet-light); }

.terminal-dialog {
width: min(720px, 100%);
Expand Down