Skip to content

Commit a7fe944

Browse files
committed
feat: add portfolio conversion experiments
1 parent 226b65d commit a7fe944

3 files changed

Lines changed: 136 additions & 25 deletions

File tree

src/components/Portfolio.tsx

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,33 @@ const SKILL_GROUPS: Array<{ key: keyof typeof SKILLS; label: string; path: strin
151151
{ key: "frontend", label: "interface", path: "/stack/interface" },
152152
];
153153

154+
155+
const EXPERIMENTS = {
156+
control: {
157+
label: "A/control",
158+
lede: "I build backend systems that can be understood, operated, and changed after they meet production traffic.",
159+
primary: { label: "View resume", href: "/resume", event: "hero_resume" },
160+
secondary: { label: "Contact on LinkedIn", href: "https://www.linkedin.com/in/jonathan-peris/", event: "hero_linkedin" },
161+
tertiary: { label: "See projects", href: "#workbench", event: "hero_projects" },
162+
},
163+
clarity: {
164+
label: "B/clarity",
165+
lede: "I design and ship reliable .NET platforms for fintech, cloud, and high-change business domains.",
166+
primary: { label: "Contact me", href: "mailto:jperis.silva@gmail.com", event: "hero_email" },
167+
secondary: { label: "View resume", href: "/resume", event: "hero_resume" },
168+
tertiary: { label: "See projects", href: "#workbench", event: "hero_projects" },
169+
},
170+
workbench: {
171+
label: "C/workbench",
172+
lede: "Architecture, delivery, and performance workbench for production systems that need clear ownership.",
173+
primary: { label: "Open workbench", href: "#workbench", event: "hero_workbench" },
174+
secondary: { label: "Contact me", href: "mailto:jperis.silva@gmail.com", event: "hero_email" },
175+
tertiary: { label: "Resume", href: "/resume", event: "hero_resume" },
176+
},
177+
} as const;
178+
179+
type ExperimentKey = keyof typeof EXPERIMENTS;
180+
154181
function projectLane(project: (typeof FEATURED_PROJECTS)[number]) {
155182
const text = `${project.name} ${project.tags.join(" ")}`.toLowerCase();
156183
if (text.includes("rinha") || text.includes("k6") || text.includes("performance")) return "load path";
@@ -162,6 +189,7 @@ function projectLane(project: (typeof FEATURED_PROJECTS)[number]) {
162189
export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
163190
const scrollProgress = useScrollProgress();
164191
const featuredSlugs = useMemo(() => new Set(FEATURED_PROJECTS.map((fp) => fp.slug)), []);
192+
const [experiment, setExperiment] = useState<ExperimentKey>("control");
165193

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

205+
useEffect(() => {
206+
const keys = Object.keys(EXPERIMENTS) as ExperimentKey[];
207+
const params = new URLSearchParams(window.location.search);
208+
const requested = params.get("ab") as ExperimentKey | null;
209+
const stored = window.localStorage.getItem("jp.heroExperiment") as ExperimentKey | null;
210+
const selected = requested && keys.includes(requested)
211+
? requested
212+
: stored && keys.includes(stored)
213+
? stored
214+
: keys[Math.floor(Math.random() * keys.length)];
215+
216+
window.localStorage.setItem("jp.heroExperiment", selected);
217+
setExperiment(selected);
218+
document.documentElement.dataset.heroExperiment = selected;
219+
trackEvent("experiment_view", { experiment: "hero_conversion", variant: selected });
220+
}, []);
221+
177222
useEffect(() => {
178223
const K = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight", "b", "a"];
179224
const onKey = (e: globalThis.KeyboardEvent) => {
@@ -202,6 +247,12 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
202247
requestAnimationFrame(() => lastFocusedRef.current?.focus());
203248
}, []);
204249

250+
const openTerminal = useCallback(() => {
251+
lastFocusedRef.current = document.activeElement as HTMLElement | null;
252+
setTermOpen(true);
253+
trackEvent("terminal_open", { source: "visible_hint" });
254+
}, []);
255+
205256
const submit = useCallback(() => {
206257
if (!termInput.trim()) return;
207258
const result = runCmd(termInput);
@@ -247,6 +298,8 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
247298
[closeTerminal, cmdHist, histIdx, submit],
248299
);
249300

301+
const activeExperiment = EXPERIMENTS[experiment];
302+
250303
return (
251304
<>
252305
<div className="scroll-bar fixed top-0 left-0 right-0 h-[2px] z-50" style={{ transform: `scaleX(${scrollProgress})` }} />
@@ -274,11 +327,14 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
274327
<h1 id="hero-title">Jonathan Peris</h1>
275328
<p className="role-line"><span>Backend architecture / .NET / Azure</span><span className="typing-cursor" aria-hidden="true" /></p>
276329
<p className="hero-lede">
277-
I build backend systems that can be understood, operated, and changed after they meet production traffic.
330+
{activeExperiment.lede}
278331
</p>
332+
<p className="experiment-badge" aria-label={`Active hero experiment ${activeExperiment.label}`}>{activeExperiment.label}</p>
279333
<div className="hero-actions">
280-
<a href="/resume" className="primary-action" onClick={() => trackEvent("cta_click", { label: "hero_resume" })}>View resume</a>
281-
<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>
334+
<a href={activeExperiment.primary.href} className="primary-action" onClick={() => trackEvent("cta_click", { label: activeExperiment.primary.event, variant: experiment })}>{activeExperiment.primary.label}</a>
335+
<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>
336+
<a href={activeExperiment.tertiary.href} className="secondary-action" onClick={() => trackEvent("cta_click", { label: activeExperiment.tertiary.event, variant: experiment })}>{activeExperiment.tertiary.label}</a>
337+
<button type="button" className="ghost-action" onClick={openTerminal}>open ~/terminal</button>
282338
</div>
283339
<div className="signal-strip" aria-label="Operating signals">
284340
{OPERATING_SIGNALS.map((signal) => (
@@ -386,10 +442,11 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
386442
</div>
387443
<h3>{project.name}</h3>
388444
<p>{project.description}</p>
445+
<p className="project-proof">{project.proof}</p>
389446
<div className="project-tags">{project.tags.map((tag) => <span key={tag}>{tag}</span>)}</div>
390447
<div className="project-actions">
391-
<a href={project.repoUrl} target="_blank" rel="noreferrer noopener">Source</a>
392-
<a href={project.liveUrl} target="_blank" rel="noreferrer noopener">Live</a>
448+
<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>
449+
<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>
393450
</div>
394451
</article>
395452
</Reveal>
@@ -427,6 +484,7 @@ export default function Portfolio({ projects }: { projects: GitHubRepo[] }) {
427484
{SOCIALS.map((social) => <SocialLink key={social.label} social={social} compact />)}
428485
</div>
429486
<p>Built as a small systems manual. Hidden shell: ↑↑↓↓←→←→BA</p>
487+
<button type="button" className="footer-terminal" onClick={openTerminal}>open ~/terminal</button>
430488
</footer>
431489
</div>
432490

src/lib/data.ts

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@ export const PROFILE = {
2020
};
2121

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

2727
export const OPERATING_SIGNALS = [
2828
{ label: "12+ yrs", value: "production software" },
2929
{ label: ".NET + Azure", value: "primary lane" },
30+
{ label: "Fintech", value: "domain depth" },
3031
{ label: "Remote", value: "Brazil to US teams" },
3132
{ label: "Systems", value: "architecture + delivery" },
3233
];
@@ -161,43 +162,58 @@ export type FeaturedProject = {
161162
slug: string;
162163
name: string;
163164
description: string;
165+
proof: string;
164166
repoUrl: string;
165167
liveUrl: string;
168+
liveLabel: string;
169+
repoLabel: string;
166170
lang: string;
167171
langColor: string;
168172
tags: string[];
169173
};
170174

171175
export const FEATURED_PROJECTS: FeaturedProject[] = [
172-
{
173-
slug: "speedy-bird-lynx",
174-
name: "Speedy Bird",
175-
description:
176-
"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.",
177-
repoUrl: "https://github.com/jonathanperis/speedy-bird-lynx",
178-
liveUrl: "https://jonathanperis.github.io/speedy-bird-lynx/",
179-
lang: "TypeScript",
180-
langColor: "#3178c6",
181-
tags: ["Lynx", "ReactLynx", "TypeScript", "Cross-Platform", "Game Dev"],
182-
},
183176
{
184177
slug: "cpnucleo",
185178
name: "Cpnucleo",
186179
description:
187-
"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.",
180+
"A production-shaped .NET 10 reference implementation for systems that need boundaries, tests, and delivery discipline instead of framework theater.",
181+
proof:
182+
"Clean Architecture, DDD, dual REST/gRPC APIs, Docker, DI, and 25+ architecture tests enforced at build time.",
188183
repoUrl: "https://github.com/jonathanperis/cpnucleo",
189184
liveUrl: "https://jonathanperis.github.io/cpnucleo/",
185+
liveLabel: "View docs",
186+
repoLabel: "View source",
190187
lang: "C#",
191188
langColor: "#178600",
192189
tags: ["Clean Architecture", ".NET", "Docker", "DI", "Testing"],
193190
},
191+
{
192+
slug: "speedy-bird-lynx",
193+
name: "Speedy Bird",
194+
description:
195+
"A Flappy Bird clone built with Lynx (ReactLynx + TypeScript), proving a single codebase can render natively across mobile and web surfaces.",
196+
proof:
197+
"Includes accelerating difficulty, medal scoring, web deployment, and a GitHub Actions delivery path for repeatable demos.",
198+
repoUrl: "https://github.com/jonathanperis/speedy-bird-lynx",
199+
liveUrl: "https://jonathanperis.github.io/speedy-bird-lynx/",
200+
liveLabel: "Play demo",
201+
repoLabel: "View source",
202+
lang: "TypeScript",
203+
langColor: "#3178c6",
204+
tags: ["Lynx", "ReactLynx", "TypeScript", "Cross-Platform", "Game Dev"],
205+
},
194206
{
195207
slug: "super-mango-editor",
196208
name: "Super Mango Editor",
197209
description:
198-
"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.",
210+
"A classic side-scrolling platformer built from scratch with C and SDL2, then compiled to WebAssembly for instant browser play.",
211+
proof:
212+
"Shows low-level runtime work: sprite animation, collision detection, Emscripten packaging, and retro gameplay constraints.",
199213
repoUrl: "https://github.com/jonathanperis/super-mango-editor",
200214
liveUrl: "https://jonathanperis.github.io/super-mango-editor/",
215+
liveLabel: "Play in browser",
216+
repoLabel: "View source",
201217
lang: "C",
202218
langColor: "#555555",
203219
tags: ["C", "SDL2", "WebAssembly", "Game Dev", "Emscripten"],
@@ -206,9 +222,13 @@ export const FEATURED_PROJECTS: FeaturedProject[] = [
206222
slug: "rinha2-back-end-dotnet",
207223
name: "Rinha de Backend 2 — .NET",
208224
description:
209-
"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).",
225+
"A concurrency-focused API built for Rinha de Backend 2024/Q1, where the useful signal is correctness under pressure, not just happy-path latency.",
226+
proof:
227+
"C#, PostgreSQL, and Nginx under strict 1.5 CPU / 550MB RAM constraints with load-oriented architecture choices.",
210228
repoUrl: "https://github.com/jonathanperis/rinha2-back-end-dotnet",
211229
liveUrl: "https://jonathanperis.github.io/rinha2-back-end-dotnet/",
230+
liveLabel: "View benchmark notes",
231+
repoLabel: "View source",
212232
lang: "C#",
213233
langColor: "#178600",
214234
tags: ["C#", "PostgreSQL", "Nginx", "High Performance", "Docker"],
@@ -217,9 +237,13 @@ export const FEATURED_PROJECTS: FeaturedProject[] = [
217237
slug: "rinha2-back-end-k6",
218238
name: "Rinha de Backend 2 — K6 Load Tests",
219239
description:
220-
"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.",
240+
"A Grafana K6 load-testing suite for validating Rinha-style APIs against realistic concurrent traffic instead of hand-wavy performance claims.",
241+
proof:
242+
"Encodes stress scenarios, endpoint validation, and repeatable pressure tests that make backend behavior observable.",
221243
repoUrl: "https://github.com/jonathanperis/rinha2-back-end-k6",
222244
liveUrl: "https://jonathanperis.github.io/rinha2-back-end-k6/",
245+
liveLabel: "Open load tests",
246+
repoLabel: "View source",
223247
lang: "JavaScript",
224248
langColor: "#f1e05a",
225249
tags: ["K6", "Load Testing", "Grafana", "Performance", "Stress Testing"],

src/styles/globals.css

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
--color-rose: oklch(72% 0.17 18);
2424
--color-amber: oklch(78% 0.16 68);
2525
--font-sans: "DM Sans", -apple-system, system-ui, sans-serif;
26-
--font-display: "DM Sans", -apple-system, system-ui, sans-serif;
26+
--font-display: "DM Serif Display", Georgia, serif;
2727
--font-mono: "JetBrains Mono", "Fira Code", monospace;
2828
}
2929

@@ -163,8 +163,8 @@ body::after {
163163
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
164164
}
165165
.code-block-accent {
166-
border-left: 3px solid var(--color-violet);
167-
box-shadow: -4px 0 20px rgba(74, 222, 128, 0.1);
166+
border-color: oklch(76% 0.18 151 / 0.28);
167+
box-shadow: inset 0 3px 0 var(--color-violet), 0 0 20px rgba(74, 222, 128, 0.1);
168168
}
169169
.code-block .titlebar {
170170
background: var(--color-elevated);
@@ -464,6 +464,19 @@ body::after {
464464
font-size: clamp(1.04rem, 2vw, 1.22rem);
465465
line-height: 1.75;
466466
}
467+
.experiment-badge {
468+
display: inline-flex;
469+
margin-top: 1rem;
470+
border: 1px solid oklch(76% 0.18 151 / 0.18);
471+
border-radius: 999px;
472+
background: oklch(94% 0.025 145 / 0.028);
473+
color: var(--color-dim);
474+
font-family: var(--font-mono);
475+
font-size: 0.68rem;
476+
letter-spacing: 0.12em;
477+
padding: 0.35rem 0.55rem;
478+
text-transform: uppercase;
479+
}
467480
.hero-actions {
468481
display: flex;
469482
flex-wrap: wrap;
@@ -727,8 +740,19 @@ body::after {
727740
letter-spacing: 0.16em;
728741
}
729742
.workbench-card > p { margin: 0.7rem 0 1rem; }
743+
.workbench-card .project-proof {
744+
border-top: 1px solid oklch(94% 0.025 145 / 0.08);
745+
border-bottom: 1px solid oklch(94% 0.025 145 / 0.08);
746+
margin: 1rem 0;
747+
padding: 0.85rem 0;
748+
color: var(--color-text);
749+
font-family: var(--font-mono);
750+
font-size: 0.76rem;
751+
line-height: 1.65;
752+
}
730753
.project-actions {
731754
display: flex;
755+
flex-wrap: wrap;
732756
gap: 0.7rem;
733757
margin-top: 1.1rem;
734758
font-family: var(--font-mono);
@@ -796,6 +820,11 @@ body::after {
796820
.icon-link:hover { color: var(--color-green); }
797821
.icon-link svg { width: 1rem; height: 1rem; }
798822
.icon-link.compact span { display: none; }
823+
.footer-terminal {
824+
color: var(--color-green);
825+
cursor: pointer;
826+
}
827+
.footer-terminal:hover { color: var(--color-violet-light); }
799828

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

0 commit comments

Comments
 (0)