From a7fe9444fede51df32f15b1c388ee72a8cbcf306 Mon Sep 17 00:00:00 2001 From: Jonathan Peris Date: Sat, 16 May 2026 23:32:21 +0000 Subject: [PATCH] feat: add portfolio conversion experiments --- src/components/Portfolio.tsx | 68 +++++++++++++++++++++++++++++++++--- src/lib/data.ts | 58 +++++++++++++++++++++--------- src/styles/globals.css | 35 +++++++++++++++++-- 3 files changed, 136 insertions(+), 25 deletions(-) diff --git a/src/components/Portfolio.tsx b/src/components/Portfolio.tsx index 9e8c0fa..b192da2 100644 --- a/src/components/Portfolio.tsx +++ b/src/components/Portfolio.tsx @@ -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"; @@ -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("control"); const [termOpen, setTermOpen] = useState(false); const [termInput, setTermInput] = useState(""); @@ -174,6 +202,23 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) { const lastFocusedRef = useRef(null); const konamiRef = useRef([]); + 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) => { @@ -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); @@ -247,6 +298,8 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) { [closeTerminal, cmdHist, histIdx, submit], ); + const activeExperiment = EXPERIMENTS[experiment]; + return ( <>
@@ -274,11 +327,14 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {

Jonathan Peris

Backend architecture / .NET / Azure

- I build backend systems that can be understood, operated, and changed after they meet production traffic. + {activeExperiment.lede}

+

{activeExperiment.label}

{OPERATING_SIGNALS.map((signal) => ( @@ -386,10 +442,11 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {

{project.name}

{project.description}

+

{project.proof}

{project.tags.map((tag) => {tag})}
@@ -427,6 +484,7 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) { {SOCIALS.map((social) => )}

Built as a small systems manual. Hidden shell: ↑↑↓↓←→←→BA

+ diff --git a/src/lib/data.ts b/src/lib/data.ts index 303208d..b5133be 100644 --- a/src/lib/data.ts +++ b/src/lib/data.ts @@ -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" }, ]; @@ -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"], @@ -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"], @@ -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"], diff --git a/src/styles/globals.css b/src/styles/globals.css index 74ccce3..0fecfd9 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -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; } @@ -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); @@ -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; @@ -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); @@ -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%);