# Pool Ortega: full site content (English)

> Pool Ortega is a Frontend Engineer based in Puebla, México, working remotely with US and LATAM teams. He has ~5 years of experience building fast, accessible sites with React, Next.js and TypeScript, including Core Web Vitals work and headless CMS migrations. He is open to frontend roles, freelance React/Next.js projects and online 1:1 programming mentoring.

## Notes

- Open to frontend roles and freelance React/Next.js projects.
- Offers online one-on-one programming mentoring in Spanish and English (JavaScript, TypeScript, React, Next.js, HTML/CSS, Node.js/Express).
- Based in Puebla, México; works remotely.
- Languages: Spanish (native), English (fluent).
- Contact: use the form on the site or LinkedIn.

## Pages

- [Pool Ortega · Frontend Developer in Mexico · React, Next.js](https://www.poolortega.com/en): Frontend Engineer in Puebla, Mexico, working remotely with US and LATAM teams. Fast, accessible React, Next.js and TypeScript sites. Open to freelance.
- [Pool Ortega · Desarrollador Frontend en Puebla, México](https://www.poolortega.com/es): Desarrollador frontend (Frontend Engineer) en Puebla, México. Sitios rápidos y accesibles con React, Next.js y TypeScript. Remoto y freelance.
- [Online Programming Mentoring (1:1) · Pool Ortega](https://www.poolortega.com/en/mentoring): Online one-on-one mentoring in JavaScript, TypeScript, React and Next.js from a Frontend Engineer and Ironhack TA. In English or Spanish.
- [Clases de programación en línea y mentoría · Pool Ortega](https://www.poolortega.com/es/mentoria): Clases particulares de programación en línea, uno a uno: JavaScript, TypeScript, React y Next.js. Mentor en Ironhack desde 2023. En español o inglés.
- [Blog · Pool Ortega, Frontend Developer (React, Next.js)](https://www.poolortega.com/en/blog): Notes on React, Next.js, accessibility and web performance from a frontend developer in Mexico who mentors web development students.
- [Blog de desarrollo frontend · Pool Ortega](https://www.poolortega.com/es/blog): Notas sobre React, Next.js, accesibilidad y rendimiento web de un desarrollador frontend en Puebla, México, y mentor de desarrollo web.
- [llms-full.txt](https://www.poolortega.com/llms-full.txt): Pool Ortega: full site content

## Blog

- [12 students, one dropdown, and the question AI couldn't answer](https://www.poolortega.com/en/blog/accessible-dropdown-ai-students) (English, 2026-09-28, [Markdown](https://www.poolortega.com/en/blog/accessible-dropdown-ai-students.md)): What an accessible dropdown needs (button, aria-expanded, keyboard, focus) and what 12 web development students taught me about AI code.
- [Not every week starts on Sunday: calendar grids with Intl.Locale](https://www.poolortega.com/en/blog/calendar-week-start-intl-locale) (English, 2026-09-22, [Markdown](https://www.poolortega.com/en/blog/calendar-week-start-intl-locale.md)): Build a calendar grid in React and TypeScript that starts the week on the right day for each locale, using Intl.Locale getWeekInfo() with a fallback.
- [12 estudiantes, un dropdown y la pregunta que la IA no pudo responder](https://www.poolortega.com/es/blog/accessible-dropdown-ai-students) (Español, 2026-09-28, [Markdown](https://www.poolortega.com/es/blog/accessible-dropdown-ai-students.md)): Qué necesita un dropdown accesible (button, aria-expanded, teclado, foco) y lo que 12 estudiantes de desarrollo web me enseñaron sobre el código de IA.
- [No todas las semanas empiezan en domingo: calendarios con Intl.Locale](https://www.poolortega.com/es/blog/calendar-week-start-intl-locale) (Español, 2026-09-22, [Markdown](https://www.poolortega.com/es/blog/calendar-week-start-intl-locale.md)): Cómo armar un calendario en React y TypeScript que empiece la semana en el día correcto según el locale, con Intl.Locale getWeekInfo() y un fallback.

## CV

- [CV English (PDF)](https://www.poolortega.com/cv/Pool-Ortega-Frontend-Engineer-en.pdf)
- [CV Español (PDF)](https://www.poolortega.com/cv/Pool-Ortega-Frontend-Engineer-es.pdf)

## Profiles

- [LinkedIn](https://www.linkedin.com/in/poolortega/)
- [GitHub](https://github.com/iPoolito)

---

## Pool Ortega · Frontend Engineer (English)

URL: https://www.poolortega.com/en

I build fast, accessible Next.js sites. Frontend Engineer in Puebla, Mexico, with ~5 years shipping React, Next.js and TypeScript for US and LATAM teams. I focus on Core Web Vitals, accessible UI, and moving marketing sites to headless CMS without breaking SEO.

- Frontend Engineer · React · Next.js · TypeScript
- Puebla, México · Remote (19.04° N · 98.20° W)
- Open to frontend roles
- Spanish (native) · English (fluent)

### Selected results

- −25%: Page load time at Commerce
- +30%: Menu interaction after nav rebuild
- 500+: Employees using a PDF export I built

### Experience

#### Software Engineer, Commerce (formerly BigCommerce)

Sep 2023 – Present · Full-time · Remote (Austin, TX)

Company website: performance, CMS migration and components marketing runs on its own.

- Cut page load times by 25% by optimizing Core Web Vitals; user engagement rose 15%.
- Rebuilt the navigation bar components and animations, increasing menu interaction by 30%.
- Reworked cookie consent to meet privacy requirements, reducing related support requests by 20%.
- Planned, executed and validated the migration of the website's content from Craft CMS to Contentful, keeping data intact.
- Built reusable visual components in Makeswift so marketing can create and update pages without engineering support.

Tech: React, Next.js, Core Web Vitals, Contentful, Makeswift

#### Software Engineer, HeroGuest

Jan 2023 – Aug 2023 · Full-time · Mexico City

Admin platform for training frontline staff in restaurants and retail.

- Built a PDF export endpoint used by 500+ employees, cutting document retrieval time by 60%.
- Fixed 30+ bugs and optimized database queries, making data retrieval 25% faster.
- Wrote the team's frontend and backend coding conventions, reducing onboarding time by 20%.

Tech: React, Node.js, SQL

#### Full-stack Developer, DivElement

Nov 2021 – Jan 2023 · Full-time · Remote (Austin, TX)

Full-stack work on a Next.js trading platform, a scheduling app and payments.

- Rewrote key components of a Next.js platform where 10,000+ players trade in-game items as NFTs, improving performance by 20%.
- Built a teacher scheduling app for a music school, cutting manual admin work by 40%.
- Integrated 5+ payment methods into a corporate website; transaction volume grew 30% in six months.

Tech: Next.js, React, Payments

### Mentoring and teaching web development

Teaching Assistant, Web Development · Ironhack (May 2023 – Present · Part-time). I mentor full-stack bootcamp students in JavaScript, React, Node, Express and MongoDB through code reviews, debugging sessions and project feedback. Before that: Tutor at Coderhouse (Jul 2022 – Jun 2023).

### Skills

- Frontend: React, Next.js, TypeScript, JavaScript, Remix, Redux, Zustand, HTML, CSS, Tailwind CSS, SCSS Modules, Web accessibility, Core Web Vitals
- Testing & quality: Jest, Playwright, Storybook
- Backend & data: Node.js, Express, Bun, GraphQL, REST, Prisma, MySQL, SQLite, MongoDB
- CMS & platforms: Contentful, Hygraph, Makeswift, Craft CMS, Vercel, AWS, Fly.io, Netlify
- Tools: Git, GitHub, Jira, Asana, Figma

### How I work

For teams and freelance clients, the same four steps every time.

1. **Measure first.** I start with Core Web Vitals and performance audits, so the work targets real problems.
2. **Plan with the team.** I agree on scope with product and design and plan changes in stages, like a CMS migration.
3. **Build reusable, accessible parts.** Components work with a keyboard and are covered by Jest, Playwright and Storybook.
4. **Ship and hand off.** I leave conventions and a CMS setup that lets marketing publish without engineering.

### Frontend developer in Puebla, Mexico

I'm a Frontend Engineer based in Puebla, Mexico, and I've spent about five years building React and Next.js apps for US and LATAM companies. My recent work sits where marketing and engineering meet: fast sites, components marketing can use without engineering help, and a CMS migration that kept every piece of content intact. I'm open to frontend roles and freelance projects.

I care about the things users feel but rarely name: load time, keyboard support, a menu that just works. Teaching at Ironhack keeps me honest about explaining why code looks the way it does. Spanish is my native language and I work fluently in English.

Education:

- B.Eng. Mechatronics Engineering, Universidad del Valle de México (2017 – 2021)
- Full-Stack Web Development Bootcamp, Ironhack, Mexico City (2021)

### Online programming mentoring, one-on-one

URL: https://www.poolortega.com/en/mentoring

One-on-one online sessions with a working Frontend Engineer who has mentored web development students for 4+ years.

#### Who it's for

- Bootcamp students and graduates who want feedback on their code
- Junior developers preparing for a frontend role
- Self-taught developers learning JavaScript, React or Next.js

#### Topics

- JavaScript
- TypeScript
- React
- Next.js
- HTML and CSS
- Node.js and Express
- Code reviews
- Preparing portfolio projects

#### Format

- Online, one-on-one
- In Spanish or English
- Details and availability by message

#### Background

- Teaching Assistant, Web Development at Ironhack since May 2023
- Tutor at Coderhouse, Jul 2022 – Jun 2023
- 4+ years mentoring through code reviews, debugging sessions and project feedback

### Let's talk

Hiring a frontend developer or have a freelance React or Next.js project? Send me a message here or reach me on LinkedIn.

- https://www.poolortega.com/en#contact
- LinkedIn: https://www.linkedin.com/in/poolortega/
- GitHub: https://github.com/iPoolito

---

## 12 students, one dropdown, and the question AI couldn't answer

> Half my students built an accessible dropdown with AI, half without. The AI group was faster. Then we tested with the keyboard.

- Pool Ortega, September 28, 2026
- URL: https://www.poolortega.com/en/blog/accessible-dropdown-ai-students
- Tags: accessibility, javascript, ai, teaching

Last week 12 students got the same exercise: build an accessible dropdown with plain HTML, CSS and JavaScript. No framework, no component library. Half of them could use AI. Half couldn't.

The AI group finished in 25 minutes on average. The other group took 60.

Then I asked everyone to use their dropdown with the keyboard only. No mouse, no trackpad.

5 of the 6 AI-built dropdowns trapped focus or skipped the options entirely. 4 of the 6 slower ones worked.

### The part that stuck with me

The keyboard results weren't the surprise. What stayed with me came after.

When I asked the AI group why they used a `div` with an `onClick` instead of a `button`, most of them couldn't answer. They hadn't chosen it. They had accepted it.

The slower group could explain every line, including the ones that were broken. That matters more than it sounds. If you can explain a broken line, you can fix it. If you never chose the line in the first place, you don't even know where to start looking.

### What an accessible dropdown actually needs

"Dropdown" covers a few different patterns, and that's part of the problem. A list of links that opens from a button is a **disclosure**. A custom `<select>` replacement is a **listbox**. An app-style list of actions is a **menu**. Each one has different keyboard rules, and mixing them is how you end up with something that looks right and behaves wrong.

For the exercise, a disclosure was enough. This is what it needs:

1. **A real `<button>`.** It's focusable, it fires on Enter and Space, and screen readers announce it as a button. A `div` gives you none of that. You end up adding `tabindex`, `role` and two key handlers to rebuild what the browser already had.
2. **`aria-expanded` on the button**, kept in sync with the list. That's how a screen reader user knows if it's open.
3. **`aria-controls`** pointing to the list, so the relationship is explicit.
4. **The list hidden for real when closed.** `hidden` or `display: none`, not just `opacity: 0`. Otherwise keyboard users tab through links they can't see.
5. **Escape closes it and sends focus back to the button.** Without that, the user is left somewhere on the page with no idea where.
6. **Tab still works normally.** Don't trap focus in a disclosure. If focus leaves the component, close it.
7. **Arrow keys are a bonus**, not a requirement, for a disclosure. If you add them, make sure they don't break Tab.

Here's a version that covers all of that:

```html
<div class="dropdown">
  <button type="button" aria-expanded="false" aria-controls="lang-list">
    Language
  </button>
  <ul id="lang-list" hidden>
    <li><a href="/en">English</a></li>
    <li><a href="/es">Español</a></li>
  </ul>
</div>
```

```js
const root = document.querySelector(".dropdown");
const button = root.querySelector("button");
const list = root.querySelector("ul");
const links = [...list.querySelectorAll("a")];

function setOpen(open) {
  button.setAttribute("aria-expanded", String(open));
  list.hidden = !open;
}

button.addEventListener("click", () => {
  setOpen(button.getAttribute("aria-expanded") !== "true");
});

root.addEventListener("keydown", (event) => {
  if (event.key === "Escape" && !list.hidden) {
    setOpen(false);
    button.focus();
    return;
  }

  // Optional: arrow keys move between links
  if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
  event.preventDefault();
  setOpen(true);

  const step = event.key === "ArrowDown" ? 1 : -1;
  const current = links.indexOf(document.activeElement);
  const next =
    current === -1
      ? step === 1 ? 0 : links.length - 1
      : (current + step + links.length) % links.length;
  links[next].focus();
});

// Close when focus moves to something outside the component (e.g. Tab)
root.addEventListener("focusout", (event) => {
  if (event.relatedTarget && !root.contains(event.relatedTarget)) {
    setOpen(false);
  }
});

// Close when clicking anywhere else on the page
document.addEventListener("click", (event) => {
  if (!root.contains(event.target)) setOpen(false);
});
```

A few things worth noticing. There's no `role="menu"` here. Adding it would promise screen reader users menu behavior (arrow keys only, Tab leaves the menu, typeahead) that this component doesn't implement. No ARIA is better than wrong ARIA.

The `focusout` handler checks `relatedTarget` before closing. Some browsers don't move focus to a button when you click it, so `relatedTarget` can be `null` on a click. The separate document click handler covers that case.

If you need a real `<select>` replacement, that's the listbox pattern, and it's more work: `role="listbox"`, `role="option"`, `aria-selected`, and managing the active option with either a roving `tabindex` or `aria-activedescendant`. Check the [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/) before building one. And honestly, ask first if a native `<select>` would do.

### The rule I added

I write React and Next.js for a living and I use AI every single day. I'm not telling anyone to stop.

I just added one rule to my class: **you can paste AI code once you can explain what it's replacing.**

If the AI gives you a `div` with an `onClick`, you should be able to say "this replaces a button, and here's what I lose". If you can't say that, you're not ready to paste it.

The AI group wasn't worse at coding. They skipped the step where you decide. The slower group made more decisions, some of them wrong, and that's exactly why they could fix them.

### What I'd hire for

Speed is easy to get now. Anyone can produce a dropdown in five minutes.

The skill I'd hire for is the second one: knowing why the code looks the way it does. That's what lets you review AI output, catch the `div` that should be a `button`, and test with a keyboard before shipping.

If you mentor or lead juniors, I'm curious: what rule have you set for AI on your team?

---

## Not every week starts on Sunday: calendar grids with Intl.Locale

> Building a calendar in React and TypeScript with native dates. How to compute the leading offset from the user's locale with getWeekInfo(), plus a fallback.

- Pool Ortega, September 22, 2026
- URL: https://www.poolortega.com/en/blog/calendar-week-start-intl-locale
- Tags: javascript, typescript, react, i18n

I built a calendar from scratch recently. React and TypeScript, no calendar UI libraries, just native JavaScript dates.

It was a good refresher on date details that are easy to forget once you stop dealing with them every day. The one I want to write about is small, and it breaks a lot of calendars: **the week doesn't start on the same day everywhere.**

### Sunday or Monday?

In the US, the week starts on Sunday. In Spain, Germany, the UK or Argentina, it starts on Monday. In Egypt or Saudi Arabia, it can be Saturday or Sunday.

It isn't even consistent inside one language. With the CLDR data that browsers use, `es-ES` starts on Monday, but `es-MX` and `es-CO` start on Sunday. Same language, different grid.

If you hardcode one convention, your calendar works for you and breaks the moment someone with a different locale opens it. The days line up under the wrong headers, or the first row has the wrong number of empty cells.

### The math that depends on it

A month grid has 7 columns. Before day 1, you render some empty cells so day 1 lands under the right weekday. That count is the **leading offset**.

`Date#getDay()` gives you the weekday of the 1st, from `0` (Sunday) to `6` (Saturday). If the week starts on Sunday, the offset is just that number. If it starts on Monday, everything shifts by one.

The general version:

```ts
function getLeadingOffset(year: number, month: number, firstDayOfWeek: number) {
  // firstDayOfWeek uses the same scale as getDay(): 0 = Sunday ... 6 = Saturday
  const firstOfMonth = new Date(year, month, 1).getDay();
  return (firstOfMonth - firstDayOfWeek + 7) % 7;
}
```

October 1, 2026 is a Thursday (`getDay()` is `4`). For `en-US` the offset is 4. For `es-ES` it's 3. Same month, different grid.

So the only real question is: where does `firstDayOfWeek` come from?

### Intl.Locale knows

The native API I hadn't touched in a while was `Intl.Locale.prototype.getWeekInfo()`. It tells you the week conventions for a locale:

```ts
new Intl.Locale("en-US").getWeekInfo();
// { firstDay: 7, weekend: [6, 7], minimalDays: 1 }

new Intl.Locale("es-ES").getWeekInfo();
// { firstDay: 1, weekend: [6, 7], minimalDays: 4 }
```

Careful with the numbers. `firstDay` goes from `1` (Monday) to `7` (Sunday), ISO style. `getDay()` goes from `0` (Sunday) to `6` (Saturday). They don't match, but `firstDay % 7` converts one to the other: 7 becomes 0, everything else stays.

There's one more catch. This API first shipped as a **getter**, `locale.weekInfo`, and was later changed to a **method**, `locale.getWeekInfo()`. Per MDN's compatibility data, the method is in Chrome 130+, Safari 17+, Firefox 153+ and Node 24+, and it reached Baseline in 2026. Older Chromium, Safari and Node versions (Node 18 to 22) only have the getter. Firefox didn't have either one before 153.

So in practice you check for both, and have a fallback for when neither exists.

### A version that works everywhere

```ts
type WeekInfo = { firstDay: number; weekend: number[]; minimalDays: number };

type LocaleWithWeekInfo = Intl.Locale & {
  getWeekInfo?: () => WeekInfo;
  weekInfo?: WeekInfo;
};

// Snapshot of CLDR regions whose week doesn't start on Monday.
const SUNDAY_FIRST = new Set(
  "AG AS BD BR BS BT BW BZ CA CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW".split(" ")
);
const SATURDAY_FIRST = new Set(
  "AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY".split(" ")
);

export function getFirstDayOfWeek(tag: string): number {
  const locale = new Intl.Locale(tag) as LocaleWithWeekInfo;

  const info =
    typeof locale.getWeekInfo === "function"
      ? locale.getWeekInfo()
      : locale.weekInfo;

  if (info) return info.firstDay % 7; // 1–7 (Mon–Sun) → 0–6 (Sun–Sat)

  // Fallback: guess from the region ("es" → "es-Latn-ES" → "ES")
  const region = locale.region ?? locale.maximize().region;
  if (region && SUNDAY_FIRST.has(region)) return 0;
  if (region && SATURDAY_FIRST.has(region)) return 6;
  return 1; // CLDR's world default is Monday
}
```

The fallback list is a snapshot of CLDR data, read through `Intl.Locale` itself. It's not something you need to keep perfect. It only runs in browsers that don't have the API, and it gets the big ones right.

Then you pass the user's locale in. In the browser, `navigator.language` is a good start. If your app already has a locale in the URL, like `/es` or `/en`, use that instead so the grid matches the rest of the page. Keep in mind that a bare `es` maximizes to Spain, so if your audience is in Mexico, `es-MX` gives a different answer.

### The weekday headers have to move too

If the grid starts on Monday, the header row must start on Monday as well. `Intl.DateTimeFormat` gives you localized names without a translation file:

```ts
export function getWeekdayLabels(tag: string, firstDayOfWeek: number) {
  const format = new Intl.DateTimeFormat(tag, { weekday: "short" });
  // January 4, 2026 is a Sunday, so day 4 + n is weekday n
  return Array.from({ length: 7 }, (_, i) =>
    format.format(new Date(2026, 0, 4 + ((firstDayOfWeek + i) % 7)))
  );
}

getWeekdayLabels("es-ES", 1); // ["lun", "mar", "mié", "jue", "vie", "sáb", "dom"]
```

Grid offset and headers come from the same `firstDayOfWeek`, so they can't get out of sync.

### What I'd keep in mind

- Never assume Sunday or Monday. Ask the locale.
- `firstDay` is 1–7, `getDay()` is 0–6. Convert once, in one place.
- Feature-detect `getWeekInfo()` and `weekInfo`. Both exist in the wild.
- `getWeekInfo()` also returns `weekend`, which is useful if you want to style weekend columns, and that varies by region too.

Native date APIs get a bad reputation, some of it deserved. But `Intl` does a lot of this work for free, and it's worth checking before you reach for a library or hardcode a convention.

---

# Pool Ortega: contenido completo del sitio (Español)

> Pool Ortega es desarrollador frontend (Frontend Engineer) en Puebla, México, y trabaja en remoto con equipos de EE. UU. y Latinoamérica. Tiene ~5 años de experiencia creando sitios rápidos y accesibles con React, Next.js y TypeScript, incluyendo trabajo en Core Web Vitals y migraciones a CMS headless. Está disponible para puestos de frontend, proyectos freelance en React/Next.js y clases particulares de programación en línea.

## Notas

- Disponible para puestos de frontend y proyectos freelance en React/Next.js.
- Da clases particulares de programación en línea, uno a uno, en español e inglés (JavaScript, TypeScript, React, Next.js, HTML/CSS, Node.js/Express).
- Vive en Puebla, México; trabaja en remoto.
- Idiomas: español (nativo), inglés (fluido).
- Contacto: el formulario del sitio o LinkedIn.

## Páginas

- [Pool Ortega · Frontend Developer in Mexico · React, Next.js](https://www.poolortega.com/en): Frontend Engineer in Puebla, Mexico, working remotely with US and LATAM teams. Fast, accessible React, Next.js and TypeScript sites. Open to freelance.
- [Pool Ortega · Desarrollador Frontend en Puebla, México](https://www.poolortega.com/es): Desarrollador frontend (Frontend Engineer) en Puebla, México. Sitios rápidos y accesibles con React, Next.js y TypeScript. Remoto y freelance.
- [Online Programming Mentoring (1:1) · Pool Ortega](https://www.poolortega.com/en/mentoring): Online one-on-one mentoring in JavaScript, TypeScript, React and Next.js from a Frontend Engineer and Ironhack TA. In English or Spanish.
- [Clases de programación en línea y mentoría · Pool Ortega](https://www.poolortega.com/es/mentoria): Clases particulares de programación en línea, uno a uno: JavaScript, TypeScript, React y Next.js. Mentor en Ironhack desde 2023. En español o inglés.
- [Blog · Pool Ortega, Frontend Developer (React, Next.js)](https://www.poolortega.com/en/blog): Notes on React, Next.js, accessibility and web performance from a frontend developer in Mexico who mentors web development students.
- [Blog de desarrollo frontend · Pool Ortega](https://www.poolortega.com/es/blog): Notas sobre React, Next.js, accesibilidad y rendimiento web de un desarrollador frontend en Puebla, México, y mentor de desarrollo web.
- [llms-full.txt](https://www.poolortega.com/llms-full.txt): Pool Ortega: contenido completo del sitio

## Blog

- [12 students, one dropdown, and the question AI couldn't answer](https://www.poolortega.com/en/blog/accessible-dropdown-ai-students) (English, 2026-09-28, [Markdown](https://www.poolortega.com/en/blog/accessible-dropdown-ai-students.md)): What an accessible dropdown needs (button, aria-expanded, keyboard, focus) and what 12 web development students taught me about AI code.
- [Not every week starts on Sunday: calendar grids with Intl.Locale](https://www.poolortega.com/en/blog/calendar-week-start-intl-locale) (English, 2026-09-22, [Markdown](https://www.poolortega.com/en/blog/calendar-week-start-intl-locale.md)): Build a calendar grid in React and TypeScript that starts the week on the right day for each locale, using Intl.Locale getWeekInfo() with a fallback.
- [12 estudiantes, un dropdown y la pregunta que la IA no pudo responder](https://www.poolortega.com/es/blog/accessible-dropdown-ai-students) (Español, 2026-09-28, [Markdown](https://www.poolortega.com/es/blog/accessible-dropdown-ai-students.md)): Qué necesita un dropdown accesible (button, aria-expanded, teclado, foco) y lo que 12 estudiantes de desarrollo web me enseñaron sobre el código de IA.
- [No todas las semanas empiezan en domingo: calendarios con Intl.Locale](https://www.poolortega.com/es/blog/calendar-week-start-intl-locale) (Español, 2026-09-22, [Markdown](https://www.poolortega.com/es/blog/calendar-week-start-intl-locale.md)): Cómo armar un calendario en React y TypeScript que empiece la semana en el día correcto según el locale, con Intl.Locale getWeekInfo() y un fallback.

## CV

- [CV English (PDF)](https://www.poolortega.com/cv/Pool-Ortega-Frontend-Engineer-en.pdf)
- [CV Español (PDF)](https://www.poolortega.com/cv/Pool-Ortega-Frontend-Engineer-es.pdf)

## Perfiles

- [LinkedIn](https://www.linkedin.com/in/poolortega/)
- [GitHub](https://github.com/iPoolito)

---

## Pool Ortega · Desarrollador Frontend (Español)

URL: https://www.poolortega.com/es

Construyo sitios Next.js rápidos y accesibles. Desarrollador frontend en Puebla, México, con ~5 años en React, Next.js y TypeScript para equipos de EE. UU. y Latinoamérica. Me enfoco en Core Web Vitals, interfaces accesibles y en migrar sitios de marketing a CMS headless sin romper el SEO.

- Desarrollador Frontend · React · Next.js · TypeScript
- Puebla, México · Remoto (19.04° N · 98.20° W)
- Disponible para roles frontend
- Español (nativo) · Inglés (fluido)

### Resultados destacados

- −25%: Tiempo de carga en Commerce
- +30%: Uso del menú tras rediseñarlo
- 500+: Empleados usan un export PDF mío

### Experiencia

#### Software Engineer, Commerce (antes BigCommerce)

sep 2023 – actualidad · Tiempo completo · Remoto (Austin, TX)

Sitio de la empresa: rendimiento, migración de CMS y componentes para marketing.

- Reduje 25% el tiempo de carga optimizando Core Web Vitals; el engagement de usuarios subió 15%.
- Rehice los componentes y animaciones de la barra de navegación; la interacción con el menú subió 30%.
- Rediseñé el consentimiento de cookies para cumplir requisitos de privacidad; los tickets de soporte relacionados bajaron 20%.
- Planeé, ejecuté y validé la migración del contenido del sitio de Craft CMS a Contentful, sin perder datos.
- Creé componentes visuales reutilizables en Makeswift para que marketing publique y edite páginas sin depender de ingeniería.

Tecnologías: React, Next.js, Core Web Vitals, Contentful, Makeswift

#### Software Engineer, HeroGuest

ene 2023 – ago 2023 · Tiempo completo · Ciudad de México

Plataforma para capacitar al personal de restaurantes y tiendas.

- Creé un endpoint de exportación a PDF que usan más de 500 empleados; obtener documentos ahora toma 60% menos tiempo.
- Corregí más de 30 bugs y optimicé consultas a la base de datos; la obtención de datos es 25% más rápida.
- Escribí las convenciones de código frontend y backend del equipo; el onboarding se redujo 20%.

Tecnologías: React, Node.js, SQL

#### Full-stack Developer, DivElement

nov 2021 – ene 2023 · Tiempo completo · Remoto (Austin, TX)

Full-stack en una plataforma Next.js de trading, una app de horarios y pagos.

- Reescribí componentes clave de una plataforma Next.js donde más de 10,000 jugadores intercambian ítems como NFTs; el rendimiento mejoró 20%.
- Desarrollé una app de horarios para maestros de una escuela de música; el trabajo administrativo manual bajó 40%.
- Integré más de 5 métodos de pago en un sitio corporativo; el volumen de transacciones creció 30% en seis meses.

Tecnologías: Next.js, React, Pagos

### Mentoría y enseñanza de desarrollo web

Teaching Assistant, Desarrollo Web · Ironhack (may 2023 – actualidad · Medio tiempo). Acompaño a estudiantes de bootcamp full-stack en JavaScript, React, Node, Express y MongoDB con code reviews, sesiones de debugging y feedback de proyectos. Antes: tutor en Coderhouse (jul 2022 – jun 2023).

### Habilidades

- Frontend: React, Next.js, TypeScript, JavaScript, Remix, Redux, Zustand, HTML, CSS, Tailwind CSS, SCSS Modules, Accesibilidad web, Core Web Vitals
- Pruebas y calidad: Jest, Playwright, Storybook
- Backend y datos: Node.js, Express, Bun, GraphQL, REST, Prisma, MySQL, SQLite, MongoDB
- CMS y plataformas: Contentful, Hygraph, Makeswift, Craft CMS, Vercel, AWS, Fly.io, Netlify
- Herramientas: Git, GitHub, Jira, Asana, Figma

### Cómo trabajo

Con equipos y clientes freelance, siempre los mismos cuatro pasos.

1. **Primero mido.** Empiezo con Core Web Vitals y auditorías de rendimiento para atacar problemas reales.
2. **Planeo con el equipo.** Acuerdo el alcance con producto y diseño, y planeo los cambios por etapas, como una migración de CMS.
3. **Construyo piezas reutilizables y accesibles.** Componentes que funcionan con teclado, cubiertos con Jest, Playwright y Storybook.
4. **Entrego y documento.** Dejo convenciones y un CMS configurado para que marketing publique sin depender de ingeniería.

### Desarrollador frontend en Puebla, México

Soy desarrollador frontend (Frontend Engineer), vivo en Puebla, México, y llevo unos cinco años creando apps en React y Next.js para empresas de EE. UU. y Latinoamérica. Mi trabajo reciente está donde se cruzan marketing e ingeniería: sitios rápidos, componentes que marketing usa sin depender de ingeniería y una migración de CMS que conservó todo el contenido intacto. Estoy abierto a puestos de frontend y a proyectos freelance.

Me importan las cosas que el usuario siente aunque no sepa nombrarlas: el tiempo de carga, que todo funcione con teclado, un menú que simplemente sirve. Dar clases en Ironhack me obliga a poder explicar por qué el código es como es. Mi idioma nativo es el español y trabajo con fluidez en inglés.

Educación:

- Ingeniería en Mecatrónica, Universidad del Valle de México (2017 – 2021)
- Bootcamp de Desarrollo Web Full-Stack, Ironhack, Ciudad de México (2021)

### Clases particulares de programación en línea

URL: https://www.poolortega.com/es/mentoria

Sesiones uno a uno en línea con un desarrollador frontend en activo que lleva más de 4 años como mentor de estudiantes de desarrollo web.

#### Para quién es

- Estudiantes y egresados de bootcamp que quieren feedback sobre su código
- Developers junior que se preparan para un puesto frontend
- Autodidactas que están aprendiendo JavaScript, React o Next.js

#### Temas

- JavaScript
- TypeScript
- React
- Next.js
- HTML y CSS
- Node.js y Express
- Code reviews
- Preparar proyectos de portafolio

#### Formato

- En línea, uno a uno
- En español o inglés
- Detalles y disponibilidad por mensaje

#### Experiencia

- Teaching Assistant de Desarrollo Web en Ironhack desde mayo de 2023
- Tutor en Coderhouse, jul 2022 – jun 2023
- Más de 4 años como mentor con code reviews, sesiones de debugging y feedback de proyectos

### Hablemos

¿Buscas un desarrollador frontend o tienes un proyecto freelance en React o Next.js? Escríbeme aquí o por LinkedIn.

- https://www.poolortega.com/es#contact
- LinkedIn: https://www.linkedin.com/in/poolortega/
- GitHub: https://github.com/iPoolito

---

## 12 estudiantes, un dropdown y la pregunta que la IA no pudo responder

> La mitad de mis alumnos hizo un dropdown accesible con IA y la otra mitad sin ella. Los de IA terminaron antes. Luego lo probamos con teclado.

- Pool Ortega, 28 de septiembre de 2026
- URL: https://www.poolortega.com/es/blog/accessible-dropdown-ai-students
- Tags: accesibilidad, javascript, ia, enseñanza

La semana pasada 12 estudiantes recibieron el mismo ejercicio: hacer un dropdown accesible con HTML, CSS y JavaScript puros. Sin framework, sin librería de componentes. La mitad podía usar IA. La otra mitad no.

El grupo con IA terminó en 25 minutos en promedio. El otro tardó 60.

Después les pedí que usaran su dropdown solo con el teclado. Nada de mouse ni trackpad.

5 de los 6 dropdowns hechos con IA atrapaban el foco o se saltaban las opciones por completo. 4 de los 6 del grupo más lento funcionaban.

### Lo que se me quedó grabado

Lo del teclado no fue lo que más me sorprendió. Lo que se me quedó fue lo que pasó después.

Cuando le pregunté al grupo con IA por qué habían usado un `div` con `onClick` en lugar de un `button`, la mayoría no supo responder. No lo habían elegido. Lo habían aceptado.

El grupo más lento podía explicar cada línea, incluso las que estaban mal. Eso importa más de lo que parece. Si puedes explicar una línea rota, puedes arreglarla. Si nunca elegiste esa línea, ni siquiera sabes por dónde empezar a buscar.

### Lo que de verdad necesita un dropdown accesible

"Dropdown" es una palabra que cubre varios patrones distintos, y ahí empieza el problema. Una lista de links que se abre desde un botón es un **disclosure**. Un reemplazo de `<select>` es un **listbox**. Una lista de acciones estilo app es un **menu**. Cada uno tiene reglas de teclado diferentes, y si los mezclas terminas con algo que se ve bien y se comporta mal.

Para el ejercicio bastaba con un disclosure. Esto es lo que necesita:

1. **Un `<button>` de verdad.** Recibe foco, se activa con Enter y con Espacio, y el lector de pantalla lo anuncia como botón. Un `div` no te da nada de eso. Terminas agregando `tabindex`, `role` y dos manejadores de teclado para reconstruir lo que el navegador ya tenía.
2. **`aria-expanded` en el botón**, sincronizado con la lista. Así sabe quien usa lector de pantalla si está abierto o cerrado.
3. **`aria-controls`** apuntando a la lista, para que la relación sea explícita.
4. **La lista oculta de verdad cuando está cerrada.** Con `hidden` o `display: none`, no solo con `opacity: 0`. Si no, con Tab llegas a links que no se ven.
5. **Escape lo cierra y regresa el foco al botón.** Sin eso, la persona queda en algún punto de la página sin saber dónde.
6. **Tab sigue funcionando normal.** No atrapes el foco en un disclosure. Si el foco sale del componente, ciérralo.
7. **Las flechas son un extra**, no un requisito, en un disclosure. Si las agregas, cuida que no rompan Tab.

Esta versión cubre todo eso:

```html
<div class="dropdown">
  <button type="button" aria-expanded="false" aria-controls="lang-list">
    Idioma
  </button>
  <ul id="lang-list" hidden>
    <li><a href="/en">English</a></li>
    <li><a href="/es">Español</a></li>
  </ul>
</div>
```

```js
const root = document.querySelector(".dropdown");
const button = root.querySelector("button");
const list = root.querySelector("ul");
const links = [...list.querySelectorAll("a")];

function setOpen(open) {
  button.setAttribute("aria-expanded", String(open));
  list.hidden = !open;
}

button.addEventListener("click", () => {
  setOpen(button.getAttribute("aria-expanded") !== "true");
});

root.addEventListener("keydown", (event) => {
  if (event.key === "Escape" && !list.hidden) {
    setOpen(false);
    button.focus();
    return;
  }

  // Opcional: las flechas mueven el foco entre los links
  if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
  event.preventDefault();
  setOpen(true);

  const step = event.key === "ArrowDown" ? 1 : -1;
  const current = links.indexOf(document.activeElement);
  const next =
    current === -1
      ? step === 1 ? 0 : links.length - 1
      : (current + step + links.length) % links.length;
  links[next].focus();
});

// Cierra cuando el foco se va fuera del componente (por ejemplo, con Tab)
root.addEventListener("focusout", (event) => {
  if (event.relatedTarget && !root.contains(event.relatedTarget)) {
    setOpen(false);
  }
});

// Cierra al hacer clic en cualquier otra parte de la página
document.addEventListener("click", (event) => {
  if (!root.contains(event.target)) setOpen(false);
});
```

Un par de detalles. Aquí no hay `role="menu"`. Si lo agregas, le prometes al lector de pantalla un comportamiento de menú (solo flechas, Tab sale del menú, búsqueda por tecleo) que este componente no tiene. Es mejor no poner ARIA que poner ARIA incorrecto.

El manejador de `focusout` revisa `relatedTarget` antes de cerrar. Algunos navegadores no le dan foco a un botón cuando le haces clic, así que `relatedTarget` puede llegar en `null`. Para ese caso está el manejador de clic en el documento.

Si lo que necesitas es reemplazar un `<select>`, ese es el patrón listbox y lleva más trabajo: `role="listbox"`, `role="option"`, `aria-selected` y manejar la opción activa con un `tabindex` dinámico o con `aria-activedescendant`. Revisa las [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/patterns/) antes de construirlo. Y, siendo honesto, pregúntate primero si un `<select>` nativo no resuelve el problema.

### La regla que agregué

Me dedico a escribir React y Next.js, y uso IA todos los días. No le estoy diciendo a nadie que deje de usarla.

Solo agregué una regla a mi clase: **puedes pegar código de IA cuando puedas explicar qué está reemplazando.**

Si la IA te da un `div` con `onClick`, deberías poder decir "esto reemplaza a un botón, y esto es lo que pierdo". Si no puedes decirlo, todavía no estás listo para pegarlo.

Al grupo con IA no le faltaba talento. Se saltaron el paso de decidir. El grupo lento tomó más decisiones, algunas equivocadas, y justo por eso pudo corregirlas.

### Lo que yo contrataría

Hoy la velocidad es fácil de conseguir. Cualquiera saca un dropdown en cinco minutos.

La habilidad que yo contrataría es la otra: entender por qué el código es como es. Eso es lo que te permite revisar lo que genera la IA, detectar el `div` que debió ser un `button` y probar con teclado antes de publicar.

Si eres mentor o lideras a juniors, me da curiosidad: ¿qué regla pusiste para el uso de IA en tu equipo?

---

## No todas las semanas empiezan en domingo: calendarios con Intl.Locale

> Un calendario en React y TypeScript con fechas nativas. Cómo calcular las celdas vacías iniciales según el locale con getWeekInfo(), con un fallback.

- Pool Ortega, 22 de septiembre de 2026
- URL: https://www.poolortega.com/es/blog/calendar-week-start-intl-locale
- Tags: javascript, typescript, react, i18n

Hace poco construí un calendario desde cero. React y TypeScript, sin librerías de calendario, solo fechas nativas de JavaScript.

Me sirvió para repasar detalles de fechas que se olvidan fácil cuando dejas de lidiar con ellos a diario. Quiero escribir sobre uno que parece menor y rompe muchísimos calendarios: **la semana no empieza el mismo día en todos lados.**

### ¿Domingo o lunes?

En Estados Unidos la semana empieza en domingo. En España, Alemania, Reino Unido o Argentina, en lunes. En Egipto o Arabia Saudita puede ser sábado o domingo.

Ni siquiera es consistente dentro del mismo idioma. Con los datos de CLDR que usan los navegadores, `es-ES` empieza en lunes, pero `es-MX` y `es-CO` empiezan en domingo. Mismo idioma, otra cuadrícula.

Si dejas fija una sola convención, el calendario te funciona a ti y se rompe en cuanto lo abre alguien con otro locale. Los días quedan bajo el encabezado equivocado o la primera fila tiene un número incorrecto de celdas vacías.

### La cuenta que depende de eso

Una cuadrícula mensual tiene 7 columnas. Antes del día 1 pintas algunas celdas vacías para que el 1 caiga bajo el día de la semana correcto. Ese número es el **offset inicial**.

`Date#getDay()` te da el día de la semana del 1, de `0` (domingo) a `6` (sábado). Si la semana empieza en domingo, el offset es ese mismo número. Si empieza en lunes, todo se recorre uno.

La versión general:

```ts
function getLeadingOffset(year: number, month: number, firstDayOfWeek: number) {
  // firstDayOfWeek usa la misma escala que getDay(): 0 = domingo ... 6 = sábado
  const firstOfMonth = new Date(year, month, 1).getDay();
  return (firstOfMonth - firstDayOfWeek + 7) % 7;
}
```

El 1 de octubre de 2026 cae en jueves (`getDay()` da `4`). Para `es-MX` el offset es 4. Para `es-ES` es 3. Mismo mes, otra cuadrícula.

Entonces la única pregunta de verdad es: ¿de dónde sale `firstDayOfWeek`?

### Intl.Locale ya lo sabe

La API nativa que no tocaba desde hacía rato era `Intl.Locale.prototype.getWeekInfo()`. Te dice las convenciones de la semana para un locale:

```ts
new Intl.Locale("es-MX").getWeekInfo();
// { firstDay: 7, weekend: [6, 7], minimalDays: 1 }

new Intl.Locale("es-ES").getWeekInfo();
// { firstDay: 1, weekend: [6, 7], minimalDays: 4 }
```

Ojo con los números. `firstDay` va de `1` (lunes) a `7` (domingo), al estilo ISO. `getDay()` va de `0` (domingo) a `6` (sábado). No coinciden, pero `firstDay % 7` convierte uno en el otro: el 7 se vuelve 0 y lo demás se queda igual.

Hay otro detalle. Esta API salió primero como **getter**, `locale.weekInfo`, y después se cambió a **método**, `locale.getWeekInfo()`. Según los datos de compatibilidad de MDN, el método está en Chrome 130+, Safari 17+, Firefox 153+ y Node 24+, y llegó a Baseline en 2026. Las versiones anteriores de Chromium, Safari y Node (de Node 18 a 22) solo tienen el getter. Firefox no tenía ninguno de los dos antes de la 153.

Así que en la práctica revisas los dos y dejas un fallback para cuando no exista ninguno.

### Una versión que funciona en todos lados

```ts
type WeekInfo = { firstDay: number; weekend: number[]; minimalDays: number };

type LocaleWithWeekInfo = Intl.Locale & {
  getWeekInfo?: () => WeekInfo;
  weekInfo?: WeekInfo;
};

// Snapshot de las regiones de CLDR donde la semana no empieza en lunes.
const SUNDAY_FIRST = new Set(
  "AG AS BD BR BS BT BW BZ CA CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW".split(" ")
);
const SATURDAY_FIRST = new Set(
  "AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY".split(" ")
);

export function getFirstDayOfWeek(tag: string): number {
  const locale = new Intl.Locale(tag) as LocaleWithWeekInfo;

  const info =
    typeof locale.getWeekInfo === "function"
      ? locale.getWeekInfo()
      : locale.weekInfo;

  if (info) return info.firstDay % 7; // 1–7 (lun–dom) → 0–6 (dom–sáb)

  // Fallback: deducirlo de la región ("es" → "es-Latn-ES" → "ES")
  const region = locale.region ?? locale.maximize().region;
  if (region && SUNDAY_FIRST.has(region)) return 0;
  if (region && SATURDAY_FIRST.has(region)) return 6;
  return 1; // el default mundial de CLDR es lunes
}
```

La lista del fallback es un snapshot de los datos de CLDR, leídos con el mismo `Intl.Locale`. No necesita ser perfecta. Solo corre en navegadores que no tienen la API, y acierta en los casos importantes.

Luego le pasas el locale del usuario. En el navegador, `navigator.language` es un buen punto de partida. Si tu app ya tiene el locale en la URL, como `/es` o `/en`, usa ese para que la cuadrícula coincida con el resto de la página. Y toma en cuenta que un `es` a secas se resuelve a España, así que si tu público está en México, `es-MX` te da otra respuesta.

### Los encabezados también se mueven

Si la cuadrícula empieza en lunes, la fila de encabezados también. `Intl.DateTimeFormat` te da los nombres localizados sin archivo de traducciones:

```ts
export function getWeekdayLabels(tag: string, firstDayOfWeek: number) {
  const format = new Intl.DateTimeFormat(tag, { weekday: "short" });
  // El 4 de enero de 2026 es domingo, así que el día 4 + n es el día de la semana n
  return Array.from({ length: 7 }, (_, i) =>
    format.format(new Date(2026, 0, 4 + ((firstDayOfWeek + i) % 7)))
  );
}

getWeekdayLabels("es-MX", 0); // ["dom", "lun", "mar", "mié", "jue", "vie", "sáb"]
```

El offset y los encabezados salen del mismo `firstDayOfWeek`, así que no se pueden desincronizar.

### Lo que me llevo

- Nunca asumas domingo o lunes. Pregúntale al locale.
- `firstDay` va de 1 a 7 y `getDay()` de 0 a 6. Convierte una sola vez, en un solo lugar.
- Detecta tanto `getWeekInfo()` como `weekInfo`. Los dos existen allá afuera.
- `getWeekInfo()` también regresa `weekend`, útil si quieres resaltar las columnas del fin de semana, que también cambian según la región.

Las APIs nativas de fechas tienen mala fama, y en parte se la ganaron. Pero `Intl` resuelve mucho de esto gratis, y vale la pena revisarlo antes de instalar una librería o dejar fija una convención.

---
