Personal portfolio of Moisés Guevara, Fullstack Developer (TypeScript · NestJS · React · Angular).
The site is a terminal running Neovim: every section of the portfolio is a Vim tab, navigable with real Vim keys — and fully usable with the mouse or on a phone for people who have never touched Vim.
README.md experience.md projects.md stack.ts contact.md cv.pdf
- Vim tabs as navigation —
gt/gT/{n}gt,:e file(with<Tab>completion),:q, or click. - Real Vim engine — normal / insert / command modes, counts (
5j),gg/G,w/b,0/$,CTRL-D/CTRL-U,/searchwithn/N,gx/<Enter>on links, Vim-accurate error messages (E492: Not an editor command). - Contact form as a buffer —
ito type,<Esc>to leave,:wto send. - Bilingual (EN / ES), auto-detected,
:set lang=esor theESbutton. - Themes — Gruvbox dark by default,
:set background=lightor◐. - Deep links (
/#/projects), browser back/forward between tabs, a boot sequence that typesnvim -p ..., file tree (<Space>e),:help.
React 19 · TypeScript (strict) · Vite · CSS Modules · Vitest + Testing Library · ESLint · Prettier. No UI or state libraries.
npm install
npm run dev # http://localhost:5173| Script | What it does |
|---|---|
npm run dev |
Dev server with HMR |
npm run build |
Type-check and build to dist/ |
npm run preview |
Serve the production build |
npm test |
Unit and integration tests |
npm run lint / typecheck / format |
Code quality |
src/
├── content/ ← YOUR DATA: profile, experience, projects, skills (typed, EN + ES)
├── buffers/ ← turns content into "files" (README.md, stack.ts…) — one module per tab
├── editor/
│ ├── vim/ ← the Vim engine: a pure reducer (keys, motions, ex commands, search)
│ ├── model/ ← buffer / line / token types and helpers
│ ├── syntax/ ← tiny highlighters: markdown, typescript, vim help
│ ├── hooks/ ← keyboard, URL hash, side-effects runner, viewport size
│ └── components/ ← TabLine, BufferView, StatusLine, CommandLine, FileTree…
├── app/ ← terminal window, boot sequence, preferences
├── i18n/ ← locales and UI messages
├── services/ ← contact form delivery
└── styles/ ← theme tokens (Gruvbox dark/light) and globals
Key decisions:
- The editor is a pure reducer (
editor/vim/reducer.ts). It never touches the DOM or the network; it queueseffects(open URL, send form, change language) thatuseEffectsRunnerexecutes. That keeps all Vim behaviour unit-testable. - The reducer is locale-agnostic — it emits message keys, components translate them.
- Content is data, not markup. Buffers are generated from
src/content/*, so updating the CV means editing one typed object, in both languages, in one place.
- Jobs →
src/content/experience.ts· Projects →src/content/projects.ts - Skills →
src/content/skills.ts· Bio, links, languages →src/content/profile.ts - CV PDFs →
public/cv/(regenerate from~/Proyectos/job-hunt/cv/build.shand copy them over)
- Create
src/buffers/<name>.tsexporting(locale) => BufferSource. - Register it in
src/buffers/index.ts(and inSTARTUP_TABSto open it on load).
Set VITE_CONTACT_ENDPOINT (see .env.example) to any endpoint that accepts a JSON POST
with { name, email, message } — e.g. a free Formspree form. Without
it, the form falls back to opening the visitor's mail client.
Any static host works; routing is hash-based, so no rewrites are needed.
- Vercel / Netlify / Cloudflare Pages — import the repo, build
npm run build, outputdist. AddVITE_CONTACT_ENDPOINTas an environment variable. - GitHub Pages under
/<repo>/— build withnpx vite build --base=/<repo>/.
MIT