Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
29 lines (27 loc) · 79 KB
/
Copy pathindex.html
File metadata and controls
29 lines (27 loc) · 79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html><html lang="en-NZ" class="dark bg-background archivo_9443aef7-module__vKFuNW__variable instrument_serif_267e791-module__2oylma__variable"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/75affa71d1e2f6a7-s.p.17-aodiw50953.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/7ebf22b5a21034f8-s.p.3j3877k49yy0l.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/_next/static/media/e41d5df559864f9e-s.p.1g73gv09-xcb6.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="preload" href="/avatar.png?v=8f9de9d818" as="image"/><link rel="preload" as="image" href="/icons/lookahead.png?v=0312c2b943"/><link rel="preload" as="image" href="/icons/xe1af35.png?v=8499e28e8b"/><link rel="preload" as="image" href="/icons/bodycap.png?v=07305f9832"/><link rel="preload" as="image" href="/icons/hotshoe.png?v=82505950b6"/><link rel="stylesheet" href="/_next/static/chunks/3rxeh6zxswvw2.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/1t4a0pwsfe2kd.js"/><script src="/_next/static/chunks/2cs-piiv5the4.js" async=""></script><script src="/_next/static/chunks/16dbq9zph9u83.js" async=""></script><script src="/_next/static/chunks/turbopack-0x_aiuwc9akfd.js" async=""></script><script src="/_next/static/chunks/0r9n-jexni0q8.js" async=""></script><script src="/_next/static/chunks/2sulsbvb27rni.js" async=""></script><meta name="theme-color" content="#25211d"/><meta name="color-scheme" content="dark"/><title>V. Ha Ly</title><meta name="description" content="Projects by V. Ha Ly, covering AI agents, systems programming, networking, and data tools, with why I built each one and what I learnt."/><link rel="icon" href="/avatar.png" type="image/png"/><link rel="apple-touch-icon" href="/avatar.png"/><meta name="next-size-adjust" content=""/><script src="/_next/static/chunks/0cz1d0mv5g_q7.js" noModule=""></script></head><body class="font-sans antialiased"><div hidden=""><!--$--><!--/$--></div><main id="top" class="mx-auto min-h-dvh w-full max-w-6xl px-4 py-6 sm:px-8 sm:py-8 lg:px-12"><section><div class="flex items-center justify-between gap-4"><p class="min-w-0 truncate font-serif text-xl italic text-foreground/90 sm:text-2xl"></p><div class="flex shrink-0 items-center gap-4 sm:gap-8"><nav aria-label="Sections"><ul class="flex gap-4 sm:gap-8"><li><a href="#projects" class="group relative py-1 text-[0.7rem] font-medium uppercase tracking-[0.18em] text-muted-foreground sm:tracking-[0.25em] transition-colors hover:text-primary">Projects<span class="absolute -bottom-0.5 left-0 h-px w-full origin-left scale-x-0 bg-primary transition-transform duration-300 group-hover:scale-x-100"></span></a></li><li><a href="#contact" class="group relative py-1 text-[0.7rem] font-medium uppercase tracking-[0.18em] text-muted-foreground sm:tracking-[0.25em] transition-colors hover:text-primary">Contact<span class="absolute -bottom-0.5 left-0 h-px w-full origin-left scale-x-0 bg-primary transition-transform duration-300 group-hover:scale-x-100"></span></a></li></ul></nav><a href="https://github.com/havl-code/havl-code.github.io" target="_blank" rel="noreferrer" aria-label="View this site on GitHub" title="View this site on GitHub" class="inline-flex size-10 shrink-0 items-center justify-center border border-border text-foreground transition-colors hover:border-primary hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg></a></div></div><p class="reveal mt-12 text-[0.7rem] font-medium uppercase tracking-[0.3em] text-primary sm:mt-20 lg:mt-28" style="animation-delay:80ms">Hi there, I'm</p><h1 class="reveal mt-6 flex flex-wrap gap-x-[0.22em] font-serif text-[3.75rem] leading-[0.9] tracking-[-0.02em] text-foreground sm:text-[6rem] lg:text-[8rem]" style="animation-delay:160ms"><span class="whitespace-nowrap">V. Ha</span><span class="italic text-primary">Ly</span><img src="/avatar.png?v=8f9de9d818" alt="" width="320" height="320" class="ml-[0.04em] size-[0.8em] self-center transition-transform duration-300 hover:-rotate-6 hover:scale-105"/></h1><p class="reveal mt-8 max-w-2xl text-pretty text-lg leading-relaxed text-muted-foreground sm:text-xl" style="animation-delay:240ms">I'm a Computer and Information Sciences graduate with a double major in Data Science and Networks & Cybersecurity. Below is a selection of projects I've built, along with why I made each one and what I learnt along the way.</p><div class="reveal mt-10 flex flex-wrap gap-3" style="animation-delay:320ms"><a href="https://github.com/havl-code" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2.5 bg-primary px-5 py-3 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View my GitHub<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a><a href="https://www.linkedin.com/in/viet-ha-ly/" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2.5 border border-border px-5 py-3 text-sm font-medium text-foreground transition-colors hover:border-primary hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13zM7.12 20.45H3.56V9h3.56v11.45zM22.23 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.73C24 .77 23.2 0 22.22 0z"></path></svg>Connect on LinkedIn<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div></section><section id="projects" aria-label="Projects" class="mt-20 scroll-mt-6 sm:mt-28 lg:mt-36"><div class="border-t border-border py-10 sm:py-12"><div class="flex flex-wrap items-baseline justify-between gap-4"><h2 class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">Filter projects</h2><p class="font-mono text-[0.7rem] text-muted-foreground" aria-live="polite">Showing <!-- -->8<!-- --> of <!-- -->8</p></div><div class="mt-6 space-y-4"><div class="grid gap-3 sm:grid-cols-[7rem_1fr] sm:gap-6"><p class="pt-1.5 text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Languages</p><div class="flex flex-wrap gap-2" role="group" aria-label="Languages"><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Python<span class="opacity-60 tabular-nums">5</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">CSS<span class="opacity-60 tabular-nums">4</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">HTML<span class="opacity-60 tabular-nums">4</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">TypeScript<span class="opacity-60 tabular-nums">3</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">JavaScript<span class="opacity-60 tabular-nums">2</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">C<span class="opacity-60 tabular-nums">1</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">SQL<span class="opacity-60 tabular-nums">1</span></button></div></div><div class="grid gap-3 sm:grid-cols-[7rem_1fr] sm:gap-6"><p class="pt-1.5 text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Tools</p><div class="flex flex-wrap gap-2" role="group" aria-label="Tools"><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Dexie<span class="opacity-60 tabular-nums">3</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Linux<span class="opacity-60 tabular-nums">3</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">React<span class="opacity-60 tabular-nums">3</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Vite<span class="opacity-60 tabular-nums">3</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">GitHub Pages<span class="opacity-60 tabular-nums">2</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Tailwind CSS<span class="opacity-60 tabular-nums">2</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Flask<span class="opacity-60 tabular-nums">1</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">LangChain<span class="opacity-60 tabular-nums">1</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">LangGraph<span class="opacity-60 tabular-nums">1</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">MapLibre<span class="opacity-60 tabular-nums">1</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Ollama<span class="opacity-60 tabular-nums">1</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Playwright MCP<span class="opacity-60 tabular-nums">1</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Plotly<span class="opacity-60 tabular-nums">1</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">Scapy<span class="opacity-60 tabular-nums">1</span></button><button type="button" aria-pressed="false" class="inline-flex items-center gap-2 border px-2.5 py-1 font-mono text-[0.7rem] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary border-border text-foreground/80 hover:border-primary hover:text-primary">SQLite<span class="opacity-60 tabular-nums">1</span></button></div></div></div></div><article class="border-t border-border py-14 sm:py-20 "><div class="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:gap-12"><div class="min-w-0 lg:col-span-5"><div class="flex items-center gap-3"><span class="font-mono text-xs tabular-nums text-primary">01</span></div><h2 class="mt-3 flex items-center gap-4 font-serif text-4xl leading-none tracking-tight text-foreground sm:text-5xl"><img src="/icons/lookahead.png?v=0312c2b943" alt="" width="48" height="48" class="size-10 shrink-0 sm:size-12"/>Lookahead</h2><p class="mt-5 text-pretty leading-relaxed text-muted-foreground">A local-first training tracker for speedcubers, with a WCA-style timer, progress charts, solve analytics, and a comparison against the world rankings. It runs on your own computer, and an offline version for your phone is on the way.</p><div class="mt-6 space-y-4"><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Languages</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Languages"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Python</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">TypeScript</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">JavaScript</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">HTML</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">CSS</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">SQL</li></ul></div><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Tools</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Tools"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Flask</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">SQLite</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Plotly</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">React</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Vite</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Dexie</li></ul></div></div><div class="mt-8 flex flex-wrap gap-x-6 gap-y-3"><a href="https://github.com/havl-code/lookahead" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View code<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a><a href="https://havl-code.github.io/lookahead/" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary">Try the live demo<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div><div class="mt-8 border border-dashed border-primary/40 p-4"><p class="flex items-center gap-2 text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-timer size-3.5" aria-hidden="true"><line x1="10" x2="14" y1="2" y2="2"></line><line x1="12" x2="15" y1="14" y2="11"></line><circle cx="12" cy="14" r="8"></circle></svg>Try it here</p><p class="mt-2 text-sm leading-relaxed text-muted-foreground">This site has a mini version of its timer. Hold<!-- --> <kbd class="border border-border px-1.5 py-0.5 font-mono text-xs text-foreground">Space</kbd> <!-- -->anywhere until the time turns green, let go to start, then press any key to stop.</p><button type="button" class="group mt-3 inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary">Or open the timer<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right size-4 text-muted-foreground transition-all duration-300 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M5 12h14"></path><path d="m12 5 7 7-7 7"></path></svg></button></div></div><div class="min-w-0 lg:col-span-7"><a href="https://github.com/havl-code/lookahead" target="_blank" rel="noreferrer" class="overflow-hidden border border-border bg-card group block transition-colors hover:border-primary focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary"><div class="flex items-center gap-3 border-b border-border px-4 py-3"><div class="flex gap-1.5" aria-hidden="true"><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span></div></div><div class="relative overflow-hidden"><img src="/projects/lookahead.png?v=5ca3224dd8" alt="The Lookahead dashboard, showing a personal best, a world rank, best averages, and recent sessions" loading="lazy" class="block w-full transition-transform duration-500 ease-out group-hover:scale-[1.02]"/><span class="absolute bottom-3 right-3 inline-flex items-center gap-2 bg-background/90 px-3 py-1.5 text-xs font-medium text-foreground opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-3.5"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View <!-- -->Lookahead<!-- --> on GitHub<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-3.5" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></span></div></a><dl class="grid gap-8 sm:grid-cols-2 mt-10 lg:grid-cols-1"><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">Why I built it</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">I <a href="https://www.worldcubeassociation.org/persons/2024HALY01" target="_blank" rel="noreferrer" title="my WCA profile" class="text-foreground underline decoration-primary/60 underline-offset-4 transition-colors hover:text-primary hover:decoration-primary">compete in speedcubing<span class="sr-only"> (<!-- -->my WCA profile<!-- -->)</span></a> and wanted one place to time my solves, track my progress, and see how I compare with the world, with my data kept safely on my own computer.</dd></div><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">What I learnt</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">How to move an app from browser storage to a small local server and database, keep it fast with tens of thousands of solves, calculate statistics exactly as the WCA rules define them, and rebuild it as an offline phone app that shares the same backups.</dd></div></dl></div></div></article><article class="border-t border-border py-14 sm:py-20 "><div class="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:gap-12"><div class="min-w-0 lg:col-span-5"><div class="flex items-center gap-3"><span class="font-mono text-xs tabular-nums text-primary">02</span><a href="https://xe1af35.github.io" target="_blank" rel="noreferrer" title="Small, local-first tools for photographers" class="group inline-flex items-center gap-1.5 border border-border px-2 py-0.5 font-mono text-[0.65rem] uppercase tracking-[0.2em] text-muted-foreground transition-colors hover:border-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"><img src="/icons/xe1af35.png?v=8499e28e8b" alt="" width="14" height="14" class="size-3.5"/><span class="sr-only">Part of </span>XE1AF35<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-3" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div><h2 class="mt-3 flex items-center gap-4 font-serif text-4xl leading-none tracking-tight text-foreground sm:text-5xl"><img src="/icons/bodycap.png?v=07305f9832" alt="" width="48" height="48" class="size-10 shrink-0 sm:size-12"/>Bodycap</h2><p class="mt-5 text-pretty leading-relaxed text-muted-foreground">A local-first app for photographers to organise their camera gear, with kits, loans, photos, a maintenance log, an insurance list, and a map of which lenses fit which cameras. It works offline and never needs an account.</p><div class="mt-6 space-y-4"><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Languages</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Languages"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">TypeScript</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">HTML</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">CSS</li></ul></div><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Tools</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Tools"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">React</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Vite</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Tailwind CSS</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Dexie</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">GitHub Pages</li></ul></div></div><div class="mt-8 flex flex-wrap gap-x-6 gap-y-3"><a href="https://github.com/XE1AF35/bodycap" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View code<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a><a href="https://xe1af35.github.io/bodycap/" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary">Try the live demo<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div></div><div class="min-w-0 lg:col-span-7"><a href="https://github.com/XE1AF35/bodycap" target="_blank" rel="noreferrer" class="overflow-hidden border border-border bg-card group block transition-colors hover:border-primary focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary"><div class="flex items-center gap-3 border-b border-border px-4 py-3"><div class="flex gap-1.5" aria-hidden="true"><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span></div></div><div class="relative overflow-hidden"><img src="/projects/bodycap.png?v=59fda9c60c" alt="The Bodycap dashboard, with a warranty running out, gear counts, a camera on loan, what the gear is worth by category, and backup status" loading="lazy" class="block w-full transition-transform duration-500 ease-out group-hover:scale-[1.02]"/><span class="absolute bottom-3 right-3 inline-flex items-center gap-2 bg-background/90 px-3 py-1.5 text-xs font-medium text-foreground opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-3.5"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View <!-- -->Bodycap<!-- --> on GitHub<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-3.5" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></span></div></a></div></div><dl class="grid gap-8 sm:grid-cols-2 mt-10 lg:mt-12 lg:gap-12"><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">Why I built it</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">I take photos and wanted one place to keep track of my gear, what's lent out, and what fits with what, without handing serial numbers and purchase details to someone else's server.</dd></div><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">What I learnt</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">How to build an offline-first web app that installs like a native one, keep data safe in the browser with backups, restores, and merges, and treat every backup file and shared link as untrusted input.</dd></div></dl></article><article class="border-t border-border py-14 sm:py-20 "><div class="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:gap-12"><div class="min-w-0 lg:col-span-5"><div class="flex items-center gap-3"><span class="font-mono text-xs tabular-nums text-primary">03</span><a href="https://xe1af35.github.io" target="_blank" rel="noreferrer" title="Small, local-first tools for photographers" class="group inline-flex items-center gap-1.5 border border-border px-2 py-0.5 font-mono text-[0.65rem] uppercase tracking-[0.2em] text-muted-foreground transition-colors hover:border-primary hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"><img src="/icons/xe1af35.png?v=8499e28e8b" alt="" width="14" height="14" class="size-3.5"/><span class="sr-only">Part of </span>XE1AF35<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-3" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div><h2 class="mt-3 flex items-center gap-4 font-serif text-4xl leading-none tracking-tight text-foreground sm:text-5xl"><img src="/icons/hotshoe.png?v=82505950b6" alt="" width="48" height="48" class="size-10 shrink-0 sm:size-12"/>Hotshoe</h2><p class="mt-5 text-pretty leading-relaxed text-muted-foreground">A small iPhone web app that logs where you took each photo, so you can geotag it later. One tap saves your location, then you can copy the coordinates or export a GPX track for Lightroom, darktable, or digiKam. It works offline and never needs an account.</p><div class="mt-6 space-y-4"><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Languages</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Languages"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">TypeScript</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">HTML</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">CSS</li></ul></div><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Tools</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Tools"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">React</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Vite</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Tailwind CSS</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Dexie</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">MapLibre</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">GitHub Pages</li></ul></div></div><div class="mt-8 flex flex-wrap gap-x-6 gap-y-3"><a href="https://github.com/XE1AF35/hotshoe" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View code<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a><a href="https://xe1af35.github.io/hotshoe/" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary">Try the live demo<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div></div><div class="min-w-0 lg:col-span-7"><a href="https://github.com/XE1AF35/hotshoe" target="_blank" rel="noreferrer" class="overflow-hidden border border-border bg-card group block transition-colors hover:border-primary focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary"><div class="flex items-center gap-3 border-b border-border px-4 py-3"><div class="flex gap-1.5" aria-hidden="true"><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span></div></div><div class="relative overflow-hidden"><img src="/projects/hotshoe.png?v=be4371cf8e" alt="Hotshoe on a phone: a location just logged with its accuracy and a label, the entries grouped by day and trip with photo thumbnails, and a map of every entry" loading="lazy" class="block w-full transition-transform duration-500 ease-out group-hover:scale-[1.02]"/><span class="absolute bottom-3 right-3 inline-flex items-center gap-2 bg-background/90 px-3 py-1.5 text-xs font-medium text-foreground opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-3.5"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View <!-- -->Hotshoe<!-- --> on GitHub<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-3.5" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></span></div></a></div></div><dl class="grid gap-8 sm:grid-cols-2 mt-10 lg:mt-12 lg:gap-12"><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">Why I built it</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">My camera doesn't record where I take a photo, so I wanted a quick way to note the spot on my phone as I shoot and add it to the photos later, with my locations kept on my phone rather than in someone else's app.</dd></div><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">What I learnt</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">How to get a trustworthy fix from the browser's location service, write GPX files that photo software lines up correctly across time zones and drifting camera clocks, and make an installed iPhone web app work offline and stay usable at the largest text sizes.</dd></div></dl></article><article class="border-t border-border py-14 sm:py-20 "><div class="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:gap-12"><div class="min-w-0 lg:col-span-5"><div class="flex items-center gap-3"><span class="font-mono text-xs tabular-nums text-primary">04</span></div><h2 class="mt-3 flex items-center gap-4 font-serif text-4xl leading-none tracking-tight text-foreground sm:text-5xl">sidekick</h2><p class="mt-5 text-pretty leading-relaxed text-muted-foreground">A personal AI co-worker that browses the web, researches, and writes files, then has an evaluator check the work before handing it back.</p><div class="mt-6 space-y-4"><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Languages</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Languages"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Python</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">JavaScript</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">HTML</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">CSS</li></ul></div><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Tools</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Tools"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">LangGraph</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">LangChain</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Playwright MCP</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">FastAPI</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">OpenAI API</li></ul></div></div><div class="mt-8 flex flex-wrap gap-x-6 gap-y-3"><a href="https://github.com/havl-code/sidekick" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View code<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div></div><div class="min-w-0 lg:col-span-7"><a href="https://github.com/havl-code/sidekick" target="_blank" rel="noreferrer" class="overflow-hidden border border-border bg-card group block transition-colors hover:border-primary focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-primary"><div class="flex items-center gap-3 border-b border-border px-4 py-3"><div class="flex gap-1.5" aria-hidden="true"><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span></div></div><div class="relative overflow-hidden"><img src="/projects/sidekick.png?v=47c875dd3c" alt="The Sidekick workspace, with a request box, optional success criteria, suggested tasks, and panels for the plan and generated files" loading="lazy" class="block w-full transition-transform duration-500 ease-out group-hover:scale-[1.02]"/><span class="absolute bottom-3 right-3 inline-flex items-center gap-2 bg-background/90 px-3 py-1.5 text-xs font-medium text-foreground opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-focus-visible:opacity-100"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-3.5"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View <!-- -->sidekick<!-- --> on GitHub<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-3.5" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></span></div></a></div></div><dl class="grid gap-8 sm:grid-cols-2 mt-10 lg:mt-12 lg:gap-12"><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">Why I built it</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">I wanted an assistant for everyday planning, study, travel, and job hunting that delivers a finished result rather than stopping to ask. It started from the Sidekick lab in Ed Donner's agents course, which I extended with my own backend, interface, and tools.</dd></div><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">What I learnt</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">How agent loops behave in practice: designing a fair evaluator, letting a person approve or decline actions mid-run, stopping a run safely, and cutting token use by trimming tools and old context.</dd></div></dl></article><article class="border-t border-border py-14 sm:py-20 "><div class="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:gap-12"><div class="min-w-0 lg:col-span-5"><div class="flex items-center gap-3"><span class="font-mono text-xs tabular-nums text-primary">05</span></div><h2 class="mt-3 flex items-center gap-4 font-serif text-4xl leading-none tracking-tight text-foreground sm:text-5xl">lx</h2><p class="mt-5 text-pretty leading-relaxed text-muted-foreground">A command-line tool that turns plain-English task descriptions into Linux commands, powered by a local LLM so it stays private, free, and offline.</p><div class="mt-6 space-y-4"><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Languages</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Languages"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Python</li></ul></div><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Tools</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Tools"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Ollama</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Linux</li></ul></div></div><div class="mt-8 flex flex-wrap gap-x-6 gap-y-3"><a href="https://github.com/havl-code/lx" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View code<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div></div><div class="min-w-0 lg:col-span-7"><div class="overflow-hidden border border-border bg-card"><div class="flex items-center gap-3 border-b border-border px-4 py-3"><div class="flex gap-1.5" aria-hidden="true"><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span></div><span class="font-mono text-[0.7rem] text-muted-foreground">lx</span></div><pre class="overflow-x-auto p-5 font-mono text-xs leading-relaxed text-foreground/85 sm:text-[0.8rem]"><span class="grid"><span class="col-start-1 row-start-1 ">What would you like to do? find every pdf larger than 50mb
⠋ Thinking...
Command: find . -type f -name "*.pdf" -size +50M
Explanation: This command recursively searches the current directory and its
subdirectories for files that match the PDF extension and are
larger than 50 megabytes.
Risk: LOW
Copy command to clipboard? [y/N]</span></span></pre></div></div></div><dl class="grid gap-8 sm:grid-cols-2 mt-10 lg:mt-12 lg:gap-12"><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">Why I built it</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">I didn't want to memorise every command flag, and I didn't want an AI running commands on my machine without my say. lx suggests a command, explains it, and rates the risk, but I always decide whether it runs.</dd></div><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">What I learnt</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">How to get reliable structured output from small local models using few-shot prompting, constrained decoding, JSON repair, and retries, and how to package, test, and benchmark a Python CLI.</dd></div></dl></article><article class="border-t border-border py-14 sm:py-20 "><div class="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:gap-12"><div class="min-w-0 lg:col-span-5"><div class="flex items-center gap-3"><span class="font-mono text-xs tabular-nums text-primary">06</span></div><h2 class="mt-3 flex items-center gap-4 font-serif text-4xl leading-none tracking-tight text-foreground sm:text-5xl">my-redis</h2><p class="mt-5 text-pretty leading-relaxed text-muted-foreground">A small in-memory key-value store written from scratch in C, with a non-blocking event loop, a binary wire protocol, a hash table, and key expiry.</p><div class="mt-6 space-y-4"><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Languages</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Languages"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">C</li></ul></div><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Tools</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Tools"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">TCP sockets</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">poll()</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Linux</li></ul></div></div><div class="mt-8 flex flex-wrap gap-x-6 gap-y-3"><a href="https://github.com/havl-code/my-redis" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View code<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div></div><div class="min-w-0 lg:col-span-7"><div class="overflow-hidden border border-border bg-card"><div class="flex items-center gap-3 border-b border-border px-4 py-3"><div class="flex gap-1.5" aria-hidden="true"><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span></div><span class="font-mono text-[0.7rem] text-muted-foreground">my-redis</span></div><pre class="overflow-x-auto p-5 font-mono text-xs leading-relaxed text-foreground/85 sm:text-[0.8rem]"><span class="grid"><span class="col-start-1 row-start-1 ">$ ./server &
$ ./client
server says: OK
server says: hello
server says: (integer) 1
server says: (nil)
server says: (error 1) unknown command
server says: OK
server says: (integer) 1
server says: (integer) 1
(sleeping 2s to let key2 expire...)
server says: (nil)</span></span></pre></div></div></div><dl class="grid gap-8 sm:grid-cols-2 mt-10 lg:mt-12 lg:gap-12"><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">Why I built it</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">Using Redis doesn't teach you how it works inside. I built a small, readable version in stages, following the Build Your Own Redis guide, so each piece stays easy to follow.</dd></div><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">What I learnt</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">Non-blocking socket programming and poll()-based event loops in C, designing a length-prefixed protocol, writing a chained hash table from scratch, and implementing lazy expiry.</dd></div></dl></article><article class="border-t border-border py-14 sm:py-20 "><div class="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:gap-12"><div class="min-w-0 lg:col-span-5"><div class="flex items-center gap-3"><span class="font-mono text-xs tabular-nums text-primary">07</span></div><h2 class="mt-3 flex items-center gap-4 font-serif text-4xl leading-none tracking-tight text-foreground sm:text-5xl">my-network-scanner</h2><p class="mt-5 text-pretty leading-relaxed text-muted-foreground">A Python CLI that finds live hosts with ARP, scans their ports with SYN packets, grabs service banners, and saves the results as JSON.</p><div class="mt-6 space-y-4"><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Languages</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Languages"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Python</li></ul></div><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Tools</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Tools"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Scapy</li><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Linux</li></ul></div></div><div class="mt-8 flex flex-wrap gap-x-6 gap-y-3"><a href="https://github.com/havl-code/my-network-scanner" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View code<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div></div><div class="min-w-0 lg:col-span-7"><div class="overflow-hidden border border-border bg-card"><div class="flex items-center gap-3 border-b border-border px-4 py-3"><div class="flex gap-1.5" aria-hidden="true"><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span></div><span class="font-mono text-[0.7rem] text-muted-foreground">my-network-scanner</span></div><pre class="overflow-x-auto p-5 font-mono text-xs leading-relaxed text-foreground/85 sm:text-[0.8rem]"><span class="grid"><span class="col-start-1 row-start-1 ">$ sudo .venv/bin/python scanner.py 192.168.1.0/24
[+] ARP scanning 192.168.1.0/24...
[+] Host found: 192.168.1.1 | d8:9e:f3:11:22:33
[+] SYN scanning 192.168.1.1...
[+] Results saved to results.json</span></span></pre></div><dl class="grid gap-8 sm:grid-cols-2 mt-10 lg:grid-cols-1"><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">Why I built it</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">Tools like nmap do far more, but using them doesn't show you how they work. I wanted a small, readable scanner where each step, from host discovery to banner grabbing, is easy to follow.</dd></div><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">What I learnt</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">ARP discovery, batched TCP SYN scanning, why UDP results are always less certain, rough OS guessing from TTL values, concurrent scanning with a thread pool, and saving partial results when a scan is interrupted.</dd></div></dl></div></div></article><article class="border-t border-border py-14 sm:py-20 "><div class="grid grid-cols-1 gap-10 lg:grid-cols-12 lg:gap-12"><div class="min-w-0 lg:col-span-5"><div class="flex items-center gap-3"><span class="font-mono text-xs tabular-nums text-primary">08</span></div><h2 class="mt-3 flex items-center gap-4 font-serif text-4xl leading-none tracking-tight text-foreground sm:text-5xl">SYBTPOC</h2><p class="mt-5 text-pretty leading-relaxed text-muted-foreground">Should You Buy This Piece of Clothing? An interactive questionnaire that scores a clothing item and recommends whether it is worth buying.</p><div class="mt-6 space-y-4"><div><p class="text-[0.65rem] font-medium uppercase tracking-[0.25em] text-muted-foreground">Languages</p><ul class="mt-2 flex flex-wrap gap-2" aria-label="Languages"><li class="border px-2.5 py-1 font-mono text-[0.7rem] border-border text-foreground/80">Python</li></ul></div></div><div class="mt-8 flex flex-wrap gap-x-6 gap-y-3"><a href="https://github.com/havl-code/sybtpoc" target="_blank" rel="noreferrer" class="group inline-flex items-center gap-2 text-sm font-medium text-foreground transition-colors hover:text-primary"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class="size-4"><path d="M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3"></path></svg>View code<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right size-4 text-muted-foreground transition-all duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-primary" aria-hidden="true"><path d="M7 7h10v10"></path><path d="M7 17 17 7"></path></svg></a></div></div><div class="min-w-0 lg:col-span-7"><div class="overflow-hidden border border-border bg-card"><div class="flex items-center gap-3 border-b border-border px-4 py-3"><div class="flex gap-1.5" aria-hidden="true"><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span><span class="size-2.5 rounded-full bg-muted-foreground/30"></span></div><span class="font-mono text-[0.7rem] text-muted-foreground">SYBTPOC</span></div><pre class="overflow-x-auto p-5 font-mono text-xs leading-relaxed text-foreground/85 sm:text-[0.8rem]"><span class="grid"><span class="col-start-1 row-start-1 ">$ python sybtpoc.py
Score Recommendation
0-49 Don't buy it. Save your money for a better item.
50-79 Consider buying it, but look at other options too.
80-100 A great buy! You should definitely purchase it.</span></span></pre></div><dl class="grid gap-8 sm:grid-cols-2 mt-10 lg:grid-cols-1"><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">Why I built it</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">I created it for my AP Computer Science Principles project, with the aim of encouraging people to shop more thoughtfully and sustainably.</dd></div><div><dt class="text-[0.7rem] font-medium uppercase tracking-[0.25em] text-primary">What I learnt</dt><dd class="mt-3 text-pretty leading-relaxed text-foreground/85">The fundamentals of sequencing, selection, and looping, and how to design a simple program around a real-world decision.</dd></div></dl></div></div></article></section><section id="contact" aria-label="Contact" class="scroll-mt-6 border-t border-border pt-14 sm:pt-20"><p class="text-pretty font-serif text-3xl leading-tight tracking-tight text-foreground sm:text-4xl">Have a question about one of my projects,<!-- --> <br class="hidden sm:inline"/>or an opportunity you'd like to discuss?<span class="mt-3 block sm:mt-4"><a href="mailto:vha.ly@outlook.com" class="group relative italic text-primary">Send me an email<span class="absolute -bottom-0.5 left-0 h-px w-full origin-left scale-x-100 bg-primary transition-transform duration-300 group-hover:scale-x-0"></span></a> <!-- -->and I'll get back to you.</span></p></section><footer class="mt-24 flex flex-wrap items-center justify-between gap-x-6 gap-y-4 border-t border-border pt-6 text-[0.7rem] font-medium uppercase tracking-[0.18em] text-muted-foreground sm:mt-32"><div class="flex items-center gap-3"><p><span class="text-foreground">© <!-- -->2026<!-- --> <!-- -->V. Ha Ly</span><span class="mx-2" aria-hidden="true">·</span>All rights reserved</p><button type="button" aria-label="Open a speedcube timer" title="Fancy a solve? Hold Space anywhere" class="inline-flex size-6 items-center justify-center text-muted-foreground/50 transition-colors hover:text-primary focus-visible:text-primary"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-box size-3.5" aria-hidden="true"><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"></path><path d="m3.3 7 8.7 5 8.7-5"></path><path d="M12 22V12"></path></svg></button></div><div class="flex items-center gap-4"><p>Thanks for stopping by</p><a href="#top" aria-label="Back to top" title="Back to top" class="group inline-flex size-10 shrink-0 items-center justify-center border border-border text-foreground transition-colors hover:border-primary hover:text-primary"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up size-4 transition-transform duration-300 group-hover:-translate-y-0.5" aria-hidden="true"><path d="m5 12 7-7 7 7"></path><path d="M12 19V5"></path></svg></a></div></footer></main><!--$--><!--/$--><script src="/_next/static/chunks/1t4a0pwsfe2kd.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[47265,[\"/_next/static/chunks/0r9n-jexni0q8.js\"],\"default\"]\n3:I[10385,[\"/_next/static/chunks/0r9n-jexni0q8.js\"],\"default\"]\n4:I[17272,[\"/_next/static/chunks/0r9n-jexni0q8.js\",\"/_next/static/chunks/2sulsbvb27rni.js\"],\"Greeting\"]\nf:I[27450,[\"/_next/static/chunks/0r9n-jexni0q8.js\"],\"default\",1]\n:HL[\"/_next/static/chunks/3rxeh6zxswvw2.css\",\"style\"]\n:HL[\"/_next/static/media/75affa71d1e2f6a7-s.p.17-aodiw50953.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/7ebf22b5a21034f8-s.p.3j3877k49yy0l.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n:HL[\"/_next/static/media/e41d5df559864f9e-s.p.1g73gv09-xcb6.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n0:{\"P\":null,\"c\":[\"\",\"\"],\"q\":\"\",\"i\":false,\"f\":[[[\"\",{\"children\":[\"__PAGE__\",{},\"$undefined\",\"$undefined\",4608]},\"$undefined\",\"$undefined\",4624],[[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/3rxeh6zxswvw2.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}],[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/0r9n-jexni0q8.js\",\"async\":true,\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en-NZ\",\"className\":\"dark bg-background archivo_9443aef7-module__vKFuNW__variable instrument_serif_267e791-module__2oylma__variable\",\"children\":[\"$\",\"body\",null,{\"className\":\"font-sans antialiased\",\"children\":[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],[]],\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}],{\"children\":[[\"$\",\"$1\",\"c\",{\"children\":[[\"$\",\"main\",null,{\"id\":\"top\",\"className\":\"mx-auto min-h-dvh w-full max-w-6xl px-4 py-6 sm:px-8 sm:py-8 lg:px-12\",\"children\":[[\"$\",\"section\",null,{\"children\":[[\"$\",\"div\",null,{\"className\":\"flex items-center justify-between gap-4\",\"children\":[[\"$\",\"$L4\",null,{\"className\":\"min-w-0 truncate font-serif text-xl italic text-foreground/90 sm:text-2xl\"}],[\"$\",\"div\",null,{\"className\":\"flex shrink-0 items-center gap-4 sm:gap-8\",\"children\":[[\"$\",\"nav\",null,{\"aria-label\":\"Sections\",\"children\":[\"$\",\"ul\",null,{\"className\":\"flex gap-4 sm:gap-8\",\"children\":[[\"$\",\"li\",\"#projects\",{\"children\":[\"$\",\"a\",null,{\"href\":\"#projects\",\"className\":\"group relative py-1 text-[0.7rem] font-medium uppercase tracking-[0.18em] text-muted-foreground sm:tracking-[0.25em] transition-colors hover:text-primary\",\"children\":[\"Projects\",[\"$\",\"span\",null,{\"className\":\"absolute -bottom-0.5 left-0 h-px w-full origin-left scale-x-0 bg-primary transition-transform duration-300 group-hover:scale-x-100\"}]]}]}],[\"$\",\"li\",\"#contact\",{\"children\":[\"$\",\"a\",null,{\"href\":\"#contact\",\"className\":\"group relative py-1 text-[0.7rem] font-medium uppercase tracking-[0.18em] text-muted-foreground sm:tracking-[0.25em] transition-colors hover:text-primary\",\"children\":[\"Contact\",[\"$\",\"span\",null,{\"className\":\"absolute -bottom-0.5 left-0 h-px w-full origin-left scale-x-0 bg-primary transition-transform duration-300 group-hover:scale-x-100\"}]]}]}]]}]}],[\"$\",\"a\",null,{\"href\":\"https://github.com/havl-code/havl-code.github.io\",\"target\":\"_blank\",\"rel\":\"noreferrer\",\"aria-label\":\"View this site on GitHub\",\"title\":\"View this site on GitHub\",\"className\":\"inline-flex size-10 shrink-0 items-center justify-center border border-border text-foreground transition-colors hover:border-primary hover:text-primary\",\"children\":[\"$\",\"svg\",null,{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"aria-hidden\":\"true\",\"className\":\"size-4\",\"children\":[\"$\",\"path\",null,{\"d\":\"M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3\"}]}]}]]}]]}],\"$L5\",\"$L6\",\"$L7\",\"$L8\"]}],\"$L9\",\"$La\",\"$Lb\"]}],[\"$Lc\"],\"$Ld\"]}],{},null,false,null]},null,false,null],\"$Le\",false]],\"m\":\"$undefined\",\"G\":[\"$f\",[\"$L10\"]],\"S\":true,\"h\":null,\"r\":\"$undefined\",\"s\":\"$undefined\",\"a\":\"$undefined\",\"l\":\"$undefined\",\"p\":\"$undefined\",\"d\":\"$undefined\",\"b\":\"OqTkvnnufB-a7DoPje8tA\"}\n11:I[3776,[\"/_next/static/chunks/0r9n-jexni0q8.js\",\"/_next/static/chunks/2sulsbvb27rni.js\"],\"default\"]\n12:I[80976,[\"/_next/static/chunks/0r9n-jexni0q8.js\",\"/_next/static/chunks/2sulsbvb27rni.js\"],\"Projects\"]\n13:I[28119,[\"/_next/static/chunks/0r9n-jexni0q8.js\",\"/_next/static/chunks/2sulsbvb27rni.js\"],\"CubeTimer\"]\n14:I[40283,[\"/_next/static/chunks/0r9n-jexni0q8.js\"],\"OutletBoundary\"]\n15:\"$Sreact.suspense\"\n17:I[40283,[\"/_next/static/chunks/0r9n-jexni0q8.js\"],\"ViewportBoundary\"]\n19:I[40283,[\"/_next/static/chunks/0r9n-jexni0q8.js\"],\"MetadataBoundary\"]\n:HL[\"/avatar.png?v=8f9de9d818\",\"image\"]\n5:[\"$\",\"p\",null,{\"className\":\"reveal mt-12 text-[0.7rem] font-medium uppercase tracking-[0.3em] text-primary sm:mt-20 lg:mt-28\",\"style\":{\"animationDelay\":\"80ms\"},\"children\":\"Hi there, I'm\"}]\n6:[\"$\",\"h1\",null,{\"className\":\"reveal mt-6 flex flex-wrap gap-x-[0.22em] font-serif text-[3.75rem] leading-[0.9] tracking-[-0.02em] text-foreground sm:text-[6rem] lg:text-[8rem]\",\"style\":{\"animationDelay\":\"160ms\"},\"children\":[[\"$\",\"span\",null,{\"className\":\"whitespace-nowrap\",\"children\":\"V. Ha\"}],[\"$\",\"span\",null,{\"className\":\"italic text-primary\",\"children\":\"Ly\"}],[\"$\",\"img\",null,{\"src\":\"/avatar.png?v=8f9de9d818\",\"alt\":\"\",\"width\":320,\"height\":320,\"className\":\"ml-[0.04em] size-[0.8em] self-center transition-transform duration-300 hover:-rotate-6 hover:scale-105\"}]]}]\n7:[\"$\",\"p\",null,{\"className\":\"reveal mt-8 max-w-2xl text-pretty text-lg leading-relaxed text-muted-foreground sm:text-xl\",\"style\":{\"animationDelay\":\"240ms\"},\"children\":\"I'm a Computer and Information Sciences graduate with a double major in Data Science and Networks \u0026 Cybersecurity. Below is a selection of projects I've built, along with why I made each one and what I learnt along the way.\"}]\n8:[\"$\",\"div\",null,{\"className\":\"reveal mt-10 flex flex-wrap gap-3\",\"style\":{\"animationDelay\":\"320ms\"},\"children\":[[\"$\",\"a\",null,{\"href\":\"https://github.com/havl-code\",\"target\":\"_blank\",\"rel\":\"noreferrer\",\"className\":\"group inline-flex items-center gap-2.5 bg-primary px-5 py-3 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90\",\"children\":[[\"$\",\"svg\",null,{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"aria-hidden\":\"true\",\"className\":\"size-4\",\"children\":[\"$\",\"path\",null,{\"d\":\"M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.4-1.8-1.4-1.8-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.7-1.6-2.7-.3-5.5-1.3-5.5-6 0-1.2.5-2.3 1.3-3.1-.2-.4-.6-1.6 0-3.2 0 0 1-.3 3.4 1.2a11.5 11.5 0 0 1 6 0C17.3 4.7 18.3 5 18.3 5c.6 1.6.2 2.8.1 3.2.8.8 1.3 1.9 1.3 3.1 0 4.6-2.8 5.6-5.5 5.9.5.4.9 1.1.9 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3\"}]}],\"View my GitHub\",[\"$\",\"$L11\",null,{\"ref\":\"$undefined\",\"icon\":{\"name\":\"arrow-up-right\",\"size\":24,\"node\":[[\"path\",{\"d\":\"M7 7h10v10\",\"key\":\"1tivn9\"}],[\"path\",{\"d\":\"M7 17 17 7\",\"key\":\"1vkiza\"}]]},\"className\":\"size-4 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5\",\"aria-hidden\":\"true\"}]]}],[\"$\",\"a\",null,{\"href\":\"https://www.linkedin.com/in/viet-ha-ly/\",\"target\":\"_blank\",\"rel\":\"noreferrer\",\"className\":\"group inline-flex items-center gap-2.5 border border-border px-5 py-3 text-sm font-medium text-foreground transition-colors hover:border-primary hover:text-primary\",\"children\":[[\"$\",\"svg\",null,{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"aria-hidden\":\"true\",\"className\":\"size-4\",\"children\":[\"$\",\"path\",null,{\"d\":\"M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13zM7.12 20.45H3.56V9h3.56v11.45zM22.23 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.73C24 .77 23.2 0 22.22 0z\"}]}],\"Connect on LinkedIn\",[\"$\",\"$L11\",null,{\"ref\":\"$undefined\",\"icon\":\"$8:props:children:0:props:children:2:props:icon\",\"className\":\"size-4 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5\",\"aria-hidden\":\"true\"}]]}]]}]\n9:[\"$\",\"$L12\",null,{\"assets\":{\"/projects/bodycap.png\":\"/projects/bodycap.png?v=59fda9c60c\",\"/projects/hotshoe.png\":\"/projects/hotshoe.png?v=be4371cf8e\",\"/projects/lookahead.png\":\"/projects/lookahead.png?v=5ca3224dd8\",\"/projects/sidekick.png\":\"/projects/sidekick.png?v=47c875dd3c\",\"/icons/bodycap.png\":\"/icons/bodycap.png?v=07305f9832\",\"/icons/hotshoe.png\":\"/icons/hotshoe.png?v=82505950b6\",\"/icons/lookahead.png\":\"/icons/lookahead.png?v=0312c2b943\",\"/icons/xe1af35.png\":\"/icons/xe1af35.png?v=8499e28e8b\"}}]\na:[\"$\",\"section\",null,{\"id\":\"contact\",\"aria-label\":\"Contact\",\"className\":\"scroll-mt-6 border-t border-border pt-14 sm:pt-20\",\"children\":[\"$\",\"p\",null,{\"className\":\"text-pretty font-serif text-3xl leading-tight tracking-tight text-foreground sm:text-4xl\",\"children\":[\"Have a question about one of my projects,\",\" \",[\"$\",\"br\",null,{\"className\":\"hidden sm:inline\"}],\"or an opportunity you'd like to discuss?\",[\"$\",\"span\",null,{\"className\":\"mt-3 block sm:mt-4\",\"children\":[[\"$\",\"a\",null,{\"href\":\"mailto:vha.ly@outlook.com\",\"className\":\"group relative italic text-primary\",\"children\":[\"Send me an email\",[\"$\",\"span\",null,{\"className\":\"absolute -bottom-0.5 left-0 h-px w-full origin-left scale-x-100 bg-primary transition-transform duration-300 group-hover:scale-x-0\"}]]}],\" \",\"and I'll get back to you.\"]}]]}]}]\nb:[\"$\",\"footer\",null,{\"className\":\"mt-24 flex flex-wrap items-center justify-between gap-x-6 gap-y-4 border-t border-border pt-6 text-[0.7rem] font-medium uppercase tracking-[0.18em] text-muted-foreground sm:mt-32\",\"children\":[[\"$\",\"div\",null,{\"className\":\"flex items-center gap-3\",\"children\":[[\"$\",\"p\",null,{\"children\":[[\"$\",\"span\",null,{\"className\":\"text-foreground\",\"children\":[\"© \",2026,\" \",\"V. Ha Ly\"]}],[\"$\",\"span\",null,{\"className\":\"mx-2\",\"aria-hidden\":\"true\",\"children\":\"·\"}],\"All rights reserved\"]}],[\"$\",\"$L13\",null,{}]]}],[\"$\",\"div\",null,{\"className\":\"flex items-center gap-4\",\"children\":[[\"$\",\"p\",null,{\"children\":\"Thanks for stopping by\"}],[\"$\",\"a\",null,{\"href\":\"#top\",\"aria-label\":\"Back to top\",\"title\":\"Back to top\",\"className\":\"group inline-flex size-10 shrink-0 items-center justify-center border border-border text-foreground transition-colors hover:border-primary hover:text-primary\",\"children\":[\"$\",\"$L11\",null,{\"ref\":\"$undefined\",\"icon\":{\"name\":\"arrow-up\",\"size\":24,\"node\":[[\"path\",{\"d\":\"m5 12 7-7 7 7\",\"key\":\"hav0vg\"}],[\"path\",{\"d\":\"M12 19V5\",\"key\":\"x0mq9r\"}]]},\"className\":\"size-4 transition-transform duration-300 group-hover:-translate-y-0.5\",\"aria-hidden\":\"true\"}]}]]}]]}]\nc:[\"$\",\"script\",\"script-0\",{\"src\":\"/_next/static/chunks/2sulsbvb27rni.js\",\"async\":true,\"nonce\":\"$undefined\"}]\nd:[\"$\",\"$L14\",null,{\"children\":[\"$\",\"$15\",null,{\"name\":\"Next.MetadataOutlet\",\"children\":\"$@16\"}]}]\ne:[\"$\",\"$1\",\"h\",{\"children\":[null,[\"$\",\"$L17\",null,{\"children\":\"$L18\"}],[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$L19\",null,{\"children\":[\"$\",\"$15\",null,{\"name\":\"Next.Metadata\",\"children\":\"$L1a\"}]}]}],[\"$\",\"meta\",null,{\"name\":\"next-size-adjust\",\"content\":\"\"}]]}]\n10:[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/chunks/3rxeh6zxswvw2.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]\n18:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"2\",{\"name\":\"theme-color\",\"content\":\"#25211d\"}],[\"$\",\"meta\",\"3\",{\"name\":\"color-scheme\",\"content\":\"dark\"}]]\n1b:I[79251,[\"/_next/static/chunks/0r9n-jexni0q8.js\"],\"IconMark\"]\n16:null\n1a:[[\"$\",\"title\",\"0\",{\"children\":\"V. Ha Ly\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Projects by V. Ha Ly, covering AI agents, systems programming, networking, and data tools, with why I built each one and what I learnt.\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/avatar.png\",\"type\":\"image/png\"}],[\"$\",\"link\",\"3\",{\"rel\":\"apple-touch-icon\",\"href\":\"/avatar.png\"}],[\"$\",\"$L1b\",\"4\",{}]]\n"])</script></body></html>