midudev/jscamp/main 285k tokens More Tools
```
├── .DS_Store (omitted)
├── .github/
   ├── actions/
      ├── setup-pnpm-ci-cd/
         ├── action.yml (100 tokens)
   ├── agents/
      ├── anti-hacking-agent.agent.md (300 tokens)
   ├── workflows/
      ├── 01-hello.yml (100 tokens)
      ├── 02-manual.yml (300 tokens)
      ├── 03-schedule.yml
      ├── 04-events.yml (300 tokens)
      ├── 05-ci.yml (400 tokens)
      ├── claude-code-review.yml (300 tokens)
      ├── claude-security.yml (500 tokens)
      ├── claude.yml (400 tokens)
├── .gitignore
├── 00-html-css/
   ├── background.webp
   ├── index.html (900 tokens)
   ├── styles.css (1200 tokens)
├── 01-javascript/
   ├── apply-button.js (400 tokens)
   ├── background.webp
   ├── config.js
   ├── data.json (1400 tokens)
   ├── devjobs-avatar-element.js (200 tokens)
   ├── empleos.html (1000 tokens)
   ├── fetch-data.js (200 tokens)
   ├── filters.js (100 tokens)
   ├── index.html (900 tokens)
   ├── main.js
   ├── styles.css (1700 tokens)
├── 02-react-cdn-version/
   ├── apply-button.js (400 tokens)
   ├── config.js
   ├── devjobs-avatar-element.js (200 tokens)
   ├── empleos.html (1000 tokens)
   ├── fetch-data.js (200 tokens)
   ├── filters.js (100 tokens)
   ├── index.html (900 tokens)
   ├── main.js
   ├── react.html (600 tokens)
   ├── styles.css (1700 tokens)
├── 02-react/
   ├── .gitignore (100 tokens)
   ├── README.md (200 tokens)
   ├── eslint.config.js (200 tokens)
   ├── index.html
   ├── package-lock.json (16.4k tokens)
   ├── package.json (100 tokens)
   ├── public/
      ├── background.webp
      ├── vite.svg (300 tokens)
   ├── src/
      ├── App.jsx (100 tokens)
      ├── assets/
         ├── react.svg (800 tokens)
      ├── components/
         ├── Footer.jsx
         ├── Header.jsx (100 tokens)
         ├── JobCard.jsx (200 tokens)
         ├── JobListings.jsx (100 tokens)
         ├── Link.jsx (100 tokens)
         ├── Pagination.jsx (500 tokens)
         ├── Pagination.module.css (100 tokens)
         ├── Route.jsx
         ├── SearchFormSection.jsx (900 tokens)
      ├── data.json (1400 tokens)
      ├── hooks/
         ├── useRouter.jsx (100 tokens)
      ├── index.css (1600 tokens)
      ├── main.jsx
      ├── pages/
         ├── 404.jsx
         ├── Home.jsx (800 tokens)
         ├── Search.jsx (900 tokens)
   ├── vite.config.js
├── 03-router-and-zustand/
   ├── .empty
   ├── .gitignore (100 tokens)
   ├── README.md (200 tokens)
   ├── eslint.config.js (200 tokens)
   ├── index.html
   ├── package-lock.json (16.9k tokens)
   ├── package.json (100 tokens)
   ├── public/
      ├── background.webp
      ├── vite.svg (300 tokens)
   ├── src/
      ├── App.jsx (300 tokens)
      ├── assets/
         ├── react.svg (800 tokens)
      ├── components/
         ├── Footer.jsx
         ├── Header.jsx (300 tokens)
         ├── JobCard.jsx (400 tokens)
         ├── JobCard.module.css (100 tokens)
         ├── JobListings.jsx (100 tokens)
         ├── Link.jsx
         ├── Pagination.jsx (500 tokens)
         ├── Pagination.module.css (100 tokens)
         ├── ProtectedRoute.jsx (100 tokens)
         ├── Route.jsx
         ├── SearchFormSection.jsx (900 tokens)
      ├── context/
         ├── AuthContext.jsx (100 tokens)
         ├── FavContext.jsx (200 tokens)
      ├── data.json (1400 tokens)
      ├── hooks/
         ├── useRouter.jsx (100 tokens)
      ├── index.css (1500 tokens)
      ├── main.jsx
      ├── pages/
         ├── 404.jsx
         ├── Auth.module.css (300 tokens)
         ├── Detail.jsx (800 tokens)
         ├── Detail.module.css (400 tokens)
         ├── Home.jsx (800 tokens)
         ├── Home.module.css (100 tokens)
         ├── Login.jsx (400 tokens)
         ├── ProfilePage.jsx (800 tokens)
         ├── ProfilePage.module.css (600 tokens)
         ├── Register.jsx (500 tokens)
         ├── Search.jsx (800 tokens)
         ├── Search.module.css
      ├── store/
         ├── authStore.js
         ├── favoritesStore.js (200 tokens)
   ├── vite.config.js
├── 04-express/
   ├── .gitignore
   ├── app.js (100 tokens)
   ├── app.test.js (300 tokens)
   ├── config.js
   ├── controllers/
      ├── jobs.js (200 tokens)
   ├── jobs.json (13.1k tokens)
   ├── middlewares/
      ├── cors.js (100 tokens)
   ├── models/
      ├── job.js (200 tokens)
   ├── old-index.js (200 tokens)
   ├── package-lock.json (6k tokens)
   ├── package.json (100 tokens)
   ├── routes/
      ├── jobs.js (200 tokens)
      ├── products.js
   ├── schemas/
      ├── jobs.js (100 tokens)
├── 04-node/
   ├── .empty
   ├── .env
   ├── archivo-uppercase.txt
   ├── archivo.txt
   ├── cli.js (200 tokens)
   ├── commonjs.js
   ├── index.js
   ├── manage-files.js (200 tokens)
   ├── math.js
   ├── node_modules/
      ├── .package-lock.json (100 tokens)
      ├── ms/
         ├── index.js (600 tokens)
         ├── license.md (200 tokens)
         ├── package.json (100 tokens)
         ├── readme.md (400 tokens)
   ├── output/
      ├── files/
         ├── documents/
            ├── archivo-uppercase.txt
   ├── package-lock.json (100 tokens)
   ├── package.json
   ├── server.js (400 tokens)
   ├── system-info.js (100 tokens)
├── 05-testing/
   ├── .empty
   ├── ai-e2e-test/
      ├── .env
      ├── package-lock.json (32.1k tokens)
      ├── package.json (100 tokens)
      ├── test-ai.js (300 tokens)
   ├── e2e/
      ├── .gitignore
      ├── package-lock.json (600 tokens)
      ├── package.json (100 tokens)
      ├── playwright.config.js (500 tokens)
      ├── tests/
         ├── example.spec.js (200 tokens)
├── 06-typescript/
   ├── .empty
   ├── 01-fundamentos/
      ├── 00-types.ts (100 tokens)
      ├── 01-primitivos.ts (100 tokens)
      ├── 02-arrays.ts (100 tokens)
      ├── 03-objetos.ts (100 tokens)
      ├── 04-tuplas.ts (200 tokens)
      ├── 05-any-unknown-never-void.ts (500 tokens)
   ├── 02-funciones/
      ├── 01-basicos.ts (300 tokens)
      ├── 02-type-narrowing.ts (300 tokens)
   ├── 03-interfaces-types/
      ├── 01-interfaces.ts (300 tokens)
      ├── 02-types-vs-interfaces.ts
   ├── 04-generics/
      ├── 01-generics-basicos.ts
      ├── 02-generics-avanzados.ts
   ├── 05-utility-types/
      ├── 01-utility-types.ts
      ├── 02-utility-types-avanzados.ts
   ├── example.ts (100 tokens)
├── 07-inteligencia-artificial/
   ├── backend/
      ├── .gitignore
      ├── app.js (100 tokens)
      ├── app.test.js (300 tokens)
      ├── config.js
      ├── controllers/
         ├── jobs.js (200 tokens)
      ├── jobs.json (13.1k tokens)
      ├── middlewares/
         ├── cors.js (100 tokens)
      ├── models/
         ├── job.js (200 tokens)
      ├── package-lock.json (6k tokens)
      ├── package.json (100 tokens)
      ├── routes/
         ├── ai.js (400 tokens)
         ├── jobs.js (200 tokens)
         ├── products.js
      ├── schemas/
         ├── jobs.js (100 tokens)
   ├── examples/
      ├── .agents/
         ├── skills/
            ├── frontend-design/
               ├── LICENSE.txt (2k tokens)
               ├── SKILL.md (900 tokens)
      ├── .claude/
         ├── agents/
            ├── performance-reviewer.md (2.3k tokens)
         ├── skills/
            ├── frontend-design
      ├── .cursor/
         ├── skills/
            ├── frontend-design
      ├── .empty
      ├── CLAUDE.md (300 tokens)
      ├── app.js (100 tokens)
      ├── package.json (100 tokens)
      ├── pnpm-lock.yaml (3.4k tokens)
      ├── public/
         ├── index.html (5.2k tokens)
      ├── rate-limit.js (200 tokens)
   ├── frontend/
      ├── .empty
      ├── .env
      ├── .gitignore (100 tokens)
      ├── README.md (200 tokens)
      ├── eslint.config.js (200 tokens)
      ├── index.html
      ├── package-lock.json (16.9k tokens)
      ├── package.json (100 tokens)
      ├── public/
         ├── background.webp
         ├── vite.svg (300 tokens)
      ├── src/
         ├── App.jsx (300 tokens)
         ├── assets/
            ├── react.svg (800 tokens)
         ├── components/
            ├── Footer.jsx
            ├── Header.jsx (300 tokens)
            ├── JobCard.jsx (400 tokens)
            ├── JobCard.module.css (100 tokens)
            ├── JobListings.jsx (100 tokens)
            ├── Link.jsx
            ├── Pagination.jsx (500 tokens)
            ├── Pagination.module.css (100 tokens)
            ├── ProtectedRoute.jsx (100 tokens)
            ├── Route.jsx
            ├── SearchFormSection.jsx (900 tokens)
         ├── context/
            ├── AuthContext.jsx (100 tokens)
            ├── FavContext.jsx (200 tokens)
         ├── data.json (1400 tokens)
         ├── hooks/
            ├── useAISummary.jsx (200 tokens)
            ├── useRouter.jsx (100 tokens)
         ├── index.css (1500 tokens)
         ├── main.jsx
         ├── pages/
            ├── 404.jsx
            ├── Auth.module.css (300 tokens)
            ├── Detail.jsx (900 tokens)
            ├── Detail.module.css (400 tokens)
            ├── Home.jsx (800 tokens)
            ├── Home.module.css (100 tokens)
            ├── Login.jsx (400 tokens)
            ├── ProfilePage.jsx (800 tokens)
            ├── ProfilePage.module.css (600 tokens)
            ├── Register.jsx (500 tokens)
            ├── Search.jsx (800 tokens)
            ├── Search.module.css
         ├── store/
            ├── authStore.js
            ├── favoritesStore.js (200 tokens)
      ├── vite.config.js
   ├── package-lock.json (37.5k tokens)
   ├── package.json (100 tokens)
├── 08-sql/
   ├── backend/
      ├── package-lock.json (13.5k tokens)
      ├── package.json (100 tokens)
      ├── pnpm-lock.yaml (7.8k tokens)
      ├── pnpm-workspace.yaml
      ├── src/
         ├── app.ts (400 tokens)
         ├── controllers/
            ├── job.ts (400 tokens)
         ├── db/
            ├── database.ts
            ├── seed.ts (400 tokens)
         ├── middlewares/
            ├── cors.ts (300 tokens)
            ├── validation.ts (500 tokens)
         ├── models/
            ├── job.ts (500 tokens)
         ├── routes/
            ├── jobs.ts (200 tokens)
         ├── schemas/
            ├── job.ts (400 tokens)
         ├── test-sql.ts (300 tokens)
         ├── types.ts (200 tokens)
      ├── tsconfig.json (100 tokens)
├── 09-ci-cd/
   ├── .empty
   ├── .gitignore (100 tokens)
   ├── package.json (100 tokens)
   ├── pnpm-lock.yaml (18.9k tokens)
   ├── pnpm-workspace.yaml
   ├── projects/
      ├── backend/
         ├── eslint.config.js (100 tokens)
         ├── package.json (100 tokens)
         ├── src/
            ├── app.js (600 tokens)
            ├── server.js
            ├── tasks.js (200 tokens)
         ├── test/
            ├── app.test.js (600 tokens)
            ├── tasks.test.js (200 tokens)
      ├── frontend/
         ├── eslint.config.js (200 tokens)
         ├── index.html (100 tokens)
         ├── package.json (200 tokens)
         ├── src/
            ├── App.css (1100 tokens)
            ├── App.jsx (700 tokens)
            ├── App.test.jsx (100 tokens)
            ├── main.jsx
            ├── tasks.js (200 tokens)
            ├── tasks.test.js (200 tokens)
            ├── test/
               ├── setup.js
         ├── vite.config.js
├── 10-docker/
   ├── .empty
├── README.md (800 tokens)
```


## /.github/actions/setup-pnpm-ci-cd/action.yml

```yml path="/.github/actions/setup-pnpm-ci-cd/action.yml" 
name: Setup pnpm and Node.js for CI/CD
description: Setup pnpm and Node.js for CI/CD

inputs:
  node-version:
    description: "Node.js version"
    required: false
    default: "24"
  pnpm-version:
    description: "pnpm version"
    required: false
    default: "11"

runs:
  using: 'composite'
  steps:
    - name: Setup pnpm
      uses: pnpm/action-setup@v6
      with:
        version: ${{ inputs.pnpm-version }}
        run_install: false

    - name: Setup Node.js
      uses: actions/setup-node@v6
      with:
        node-version: ${{ inputs.node-version }}

    - name: Install dependencies
      shell: bash
      run: |
        cd 09-ci-cd
        pnpm install --frozen-lockfile
```

## /.github/agents/anti-hacking-agent.agent.md

---
name: anti-hacking-agent
description: Evitar que se nos cuele algún fallo de seguridad o vulnerabilidad
argument-hint: Espera que hablemos de "problemas de seguridad", "vulnerabilidades", "fallos de seguridad" o algo similar para activarse.
# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.
---

Buscar problemas de seguridad y proponer soluciones.

## Si encuentra una llamada de SQL

- Si la llamada de SQL es vulnerable a inyección de SQL, proponer una solución utilizando consultas preparadas o procedimientos almacenados.
- Si la llamada de SQL no es vulnerable, confirmar que se han implementado medidas de seguridad adecuadas, como la validación de entradas y el uso de ORM.

## Si una API es posible que sea vulnerable a ataques de fuerza bruta

- Proponer la implementación de mecanismos de protección contra ataques de fuerza bruta, como la limitación de intentos, el uso de CAPTCHA o la autenticación multifactor.

## Si una API se puede llamar demasiado y no tiene rate limit

- Proponer la implementación de un sistema de limitación de tasa (rate limiting) para evitar abusos y proteger los recursos del servidor.
- Sugerir el uso de herramientas como Redis o Memcached para gestionar el estado de las solicitudes y aplicar la limitación de tasa de manera eficiente.

## /.github/workflows/01-hello.yml

```yml path="/.github/workflows/01-hello.yml" 
name: 01 - Hello

on:
  workflow_dispatch:

jobs:
  hello:
    runs-on: ubuntu-24.04
    steps:
      - name: Saludar
        run: echo "Hola desde GitHub Actions!"

      - name: Ver fecha del runner
        run: date

      - name: Ver archivos disponibles
        run: ls -la

  repo-files:
    runs-on: ubuntu-24.04
    steps:
      - name: Descargar código del repo
        uses: actions/checkout@v6

      - name: Ver archivos disponibles
        run: ls -la
```

## /.github/workflows/02-manual.yml

```yml path="/.github/workflows/02-manual.yml" 
name: 02 - Manual

on:
  workflow_dispatch:
    inputs:
      logging_level:
        description: "Nivel de log"
        required: true
        default: "info"
        type: choice
        options:
          - info
          - debug
          - warn
          - error
      
      environment:
        description: "Entorno"
        type: environment

      dry_run:
        description: "Ejecutar sin aplicar los cambios"
        required: false
        default: false
        type: boolean

      reason:
        description: "Motivo de la ejecución"
        required: false
        default: "Manual execution"
        type: string

jobs:
  manual:
    runs-on: ubuntu-24.04
    steps:
      - name: Mostrar todos los inputs
        env:
          LOG_LEVEL: ${{ inputs.logging_level }}
          ENVIRONMENT: ${{ inputs.environment }}
          DRY_RUN: ${{ inputs.dry_run }}
          REASON: ${{ inputs.reason }}
        run: |
          echo "LOG_LEVEL: $LOG_LEVEL"
          echo "ENVIRONMENT: $ENVIRONMENT"
          echo "DRY_RUN: $DRY_RUN"
          echo "REASON: $REASON"


      - name: Deploy (solo si no es dry-run)
        if: ${{ !inputs.dry_run }}
        env:
          ENVIRONMENT: ${{ inputs.environment }}
        run: |
          echo "Deploying to $ENVIRONMENT"

      - name: Solo log si es dry-run
        if: ${{ inputs.dry_run }}
        run: |
          echo "Dry run: no changes applied"
```

## /.github/workflows/03-schedule.yml

```yml path="/.github/workflows/03-schedule.yml" 
name: 03 - Schedule

on:
  schedule:
    - cron: '0 9 * * MON'

jobs:
  schedule:
    runs-on: ubuntu-24.04
    steps:
      - name: Saludar
        run: echo "Hola desde GitHub Actions!"
```

## /.github/workflows/04-events.yml

```yml path="/.github/workflows/04-events.yml" 
name: 04 - Events

on:
  workflow_dispatch:

jobs:
  inspect-event:
    runs-on: ubuntu-24.04
    permissions:
      issues: write
      
    steps:
      - name: Inspect event
        run: |
          echo "Event: ${{ github.event_name }}"
          echo "Issue: ${{ github.event.issue.number }}"
          echo "Comment: ${{ github.event.comment.body }}"

      - name: Comentar como bot en GitHub Actions
        if: github.event_name == 'issues' && github.event.action == 'opened'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
          GITHUB_REPOSITORY: ${{ github.repository }}
        run:
          gh issue comment "$ISSUE_NUMBER" --body "Gracias por tu aporte. Nos pondremos en contacto contigo lo antes posible." --repo "$GITHUB_REPOSITORY"

      - name: Comentar como autor del repositorio
        if: github.event_name == 'issues' && github.event.action == 'opened'
        env:
          AUTHOR_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
        run: |
          if [[ -z "$AUTHOR_TOKEN" ]]; then
            echo "::warning::No hay PERSONAL_ACCESS_TOKEN configurado. Se omite el comentario como autor."
            exit 0
          fi
          GH_TOKEN="$AUTHOR_TOKEN" gh issue comment "$ISSUE_NUMBER" \
            --repo "$GITHUB_REPOSITORY" \
            --body "Hola, soy el dueño del repositorio respondiendo personalmente. Mírate la guía de contribución mientras tanto."
```

## /.github/workflows/05-ci.yml

```yml path="/.github/workflows/05-ci.yml" 
name: 05 - CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  frontend-lint:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup-pnpm-ci-cd
      - name: Lint
        run: |
          cd 09-ci-cd
          echo "::group::Lint"
          pnpm lint:frontend
          echo "::endgroup::"

  frontend-test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup-pnpm-ci-cd
      - name: Test
        run: |
          cd 09-ci-cd
          echo "::group::Test"
          pnpm test:frontend
          echo "::endgroup::"

  frontend-build:
    needs: [frontend-lint, frontend-test]
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup-pnpm-ci-cd
      - name: Build
        run: |
          cd 09-ci-cd
          echo "::group::Build"
          pnpm build:frontend
          echo "::endgroup::"

  backend-lint:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup-pnpm-ci-cd
      - name: Lint
        run: |
          cd 09-ci-cd
          echo "::group::Lint"
          pnpm lint:backend
          echo "::endgroup::"

  backend-test:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup-pnpm-ci-cd
      - name: Test
        run: |
          cd 09-ci-cd
          echo "::group::Test"
          pnpm test:backend
          echo "::endgroup::"

  backend-smoke-test:
    needs: [backend-lint, backend-test]
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v6
      - uses: ./.github/actions/setup-pnpm-ci-cd
      - name: Smoke test
        run: |
          cd 09-ci-cd
          echo "::group::Smoke test"
          pnpm build:backend
          pnpm start:backend
          curl -X GET http://localhost:3000/health
          echo "::endgroup::"
```

## /.github/workflows/claude-code-review.yml

```yml path="/.github/workflows/claude-code-review.yml" 
name: Claude Code Review

on:
  pull_request:
    types: [opened, synchronize, ready_for_review, reopened]
    # Optional: Only run on specific file changes
    # paths:
    #   - "src/**/*.ts"
    #   - "src/**/*.tsx"
    #   - "src/**/*.js"
    #   - "src/**/*.jsx"

jobs:
  claude-review:
    # Optional: Filter by PR author
    # if: |
    #   github.event.pull_request.user.login == 'external-contributor' ||
    #   github.event.pull_request.user.login == 'new-developer' ||
    #   github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'

    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Run Claude Code Review
        id: claude-review
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
          plugins: 'code-review@claude-code-plugins'
          prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://code.claude.com/docs/en/cli-reference for available options


```

## /.github/workflows/claude-security.yml

```yml path="/.github/workflows/claude-security.yml" 
name: 10 - Claude Security Review

on:
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]

concurrency:
  group: claude-security-review-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  security-review:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    timeout-minutes: 20
    permissions:
      contents: read
      pull-requests: write
      issues: read
      id-token: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6
        with:
          fetch-depth: 1

      - name: Run Claude Security Review
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
          track_progress: true
          prompt: |
            REPO: ${{ github.repository }}
            PR NUMBER: ${{ github.event.pull_request.number }}

            Revisa la seguridad de esta pull request y publica un comentario
            en la PR con el resultado.

            Prioriza hallazgos reales y accionables. Busca especialmente:
            - Exposición de secretos, tokens, credenciales o datos sensibles.
            - Inyecciones: SQL, shell, HTML, template, prompt injection o path traversal.
            - Problemas de autenticación, autorización, permisos o escalada de privilegios.
            - Uso inseguro de dependencias, acciones de GitHub o scripts de CI.
            - Validación insuficiente de entradas externas.
            - Riesgos OWASP relevantes para los cambios de esta PR.

            Responde en español, de forma concisa. Si no encuentras riesgos claros,
            dilo explícitamente y menciona cualquier zona que no hayas podido verificar.
            No propongas cambios cosméticos ni de estilo salvo que afecten a seguridad.
            Usa `gh pr comment` para publicar un comentario final en la PR.
          claude_args: |
            --max-turns 10
            --allowedTools "Read,Grep,Glob,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
            --append-system-prompt "Actua como auditor de seguridad de aplicaciones y CI/CD. No ejecutes codigo de la PR. Revisa solo el diff y el contexto necesario. Prioriza bugs explotables, secretos, permisos y supply chain."

```

## /.github/workflows/claude.yml

```yml path="/.github/workflows/claude.yml" 
name: Claude Code

on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
  issues:
    types: [opened, assigned]
  pull_request_review:
    types: [submitted]

jobs:
  claude:
    if: |
      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write
      actions: read # Required for Claude to read CI results on PRs
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 1

      - name: Run Claude Code
        id: claude
        uses: anthropics/claude-code-action@v1
        with:
          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

          # This is an optional setting that allows Claude to read CI results on PRs
          additional_permissions: |
            actions: read

          # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
          # prompt: 'Update the pull request description to include a summary of changes.'

          # Optional: Add claude_args to customize behavior and configuration
          # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
          # or https://code.claude.com/docs/en/cli-reference for available options
          # claude_args: '--allowed-tools Bash(gh pr *)'


```

## /.gitignore

```gitignore path="/.gitignore" 
**/.DS_Store
node_modules/
```

## /00-html-css/background.webp

Binary file available at https://raw.githubusercontent.com/midudev/jscamp/refs/heads/main/00-html-css/background.webp

## /00-html-css/index.html

```html path="/00-html-css/index.html" 
<!DOCTYPE html>
<html lang="es">

<head>
  <title>DevJobs - Inicio</title>
  <meta charset="UTF-8" />
  <meta name="description" content="Encuentra las mejores ofertas de trabajo para desarrolladores en DevJobs.">

  <link rel="stylesheet" href="./styles.css" />
</head>

<body>
  <header>
    <h1>
      <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
        viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <polyline points="16 18 22 12 16 6"></polyline>
        <polyline points="8 6 2 12 8 18"></polyline>
      </svg>
      DevJobs
    </h1>

    <nav>
      <!-- <a href="">
        Inicio
      </a> -->
      <a href="empleos.html">Empleos</a>
    </nav>

    <div>
      <a href="">Publicar un empleo</a>
      <!-- <a href="">Iniciar sesión</a> -->
    </div>
  </header>

  <main>
    <section>
      <img src="./background.webp" width="200" />

      <h1>Encuentra el trabajo de tus sueños</h1>

      <p>Únete a la comunidad más grande de desarrolladores y encuentra tu próxima oportunidad.</p>

      <form role="search">
        <div>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
            stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
            class="icon icon-tabler icons-tabler-outline icon-tabler-search">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
            <path d="M21 21l-6 -6" />
          </svg>

          <input required type="text" placeholder="Buscar empleos por título, habilidad o empresa">

          <button disabled type="submit">Buscar</button>
        </div>
      </form>
    </section>

    <section>

      <header>
        <h2>¿Por qué DevJobs?</h2>
        <p>DevJobs es la principal plataforma de búsqueda de empleo para desarrolladores. Conectamos a los mejores
          talentos con las empresas más innovadoras.</p>
      </header>

      <footer>
        <article>
          <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32" xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true">
            <path
              d="M216,56H176V48a24,24,0,0,0-24-24H104A24,24,0,0,0,80,48v8H40A16,16,0,0,0,24,72V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V72A16,16,0,0,0,216,56ZM96,48a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96ZM216,72v41.61A184,184,0,0,1,128,136a184.07,184.07,0,0,1-88-22.38V72Zm0,128H40V131.64A200.19,200.19,0,0,0,128,152a200.25,200.25,0,0,0,88-20.37V200ZM104,112a8,8,0,0,1,8-8h32a8,8,0,0,1,0,16H112A8,8,0,0,1,104,112Z">
            </path>
          </svg>
          <h3>Encuentra el trabajo de tus sueños</h3>
          <p>Busca miles de empleos de las mejores empresas de todo el mundo.</p>
        </article>

        <article>
          <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32" xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true">
            <path
              d="M117.25,157.92a60,60,0,1,0-66.5,0A95.83,95.83,0,0,0,3.53,195.63a8,8,0,1,0,13.4,8.74,80,80,0,0,1,134.14,0,8,8,0,0,0,13.4-8.74A95.83,95.83,0,0,0,117.25,157.92ZM40,108a44,44,0,1,1,44,44A44.05,44.05,0,0,1,40,108Zm210.14,98.7a8,8,0,0,1-11.07-2.33A79.83,79.83,0,0,0,172,168a8,8,0,0,1,0-16,44,44,0,1,0-16.34-84.87,8,8,0,1,1-5.94-14.85,60,60,0,0,1,55.53,105.64,95.83,95.83,0,0,1,47.22,37.71A8,8,0,0,1,250.14,206.7Z">
            </path>
          </svg>
          <h3>Conecta con las mejores empresas</h3>
          <p>Conecta con empresas que están contratando por tus habilidades.</p>
        </article>

        <article>
          <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32" xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true">
            <path
              d="M240,208H224V96a16,16,0,0,0-16-16H144V32a16,16,0,0,0-24.88-13.32L39.12,72A16,16,0,0,0,32,85.34V208H16a8,8,0,0,0,0,16H240a8,8,0,0,0,0-16ZM208,96V208H144V96ZM48,85.34,128,32V208H48ZM112,112v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm-32,0v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm0,56v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Zm32,0v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Z">
            </path>
          </svg>
          <h3>Obtén el salario que mereces</h3>
          <p>Obtén el salario que mereces con nuestra calculadora de salarios.</p>
        </article>
      </footer>

    </section>
  </main>

  <footer>
    <small>&copy; 2025 DevJobs. Todos los derechos reservados.</small>
  </footer>

</body>

</html>
```

## /00-html-css/styles.css

```css path="/00-html-css/styles.css" 
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}






:root {
  --primary: #3b4550;
  --primary-dark: #0a3d66;
  --primary-hover: #0d5ba8;
  --primary-light: #09f;
  --background: #06182a;
  --text-primary: #ffffff;
  --text-secondary: #cbd5e1;
  --text-muted: #94a3b8;
  --border: rgba(255, 255, 255, 0.1);
  --card-bg: #1e293b;
  --shadow: rgba(0, 0, 0, 0.3);
  --input-bg: #1e293b;
  --hsla-example: hsla(210, 100%, 56%, 1);
}










@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(https://cdn.jsdelivr.net/fontsource/fonts/inter:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}










body {
  font-family: system-ui;
  background-color: var(--background);
  color: var(--text-primary);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  line-height: 1.6;
}













/* Títulos principales */
h1 {
  font-size: 1.5rem;
  line-height: 1.2;
  text-wrap: balance;
  display: flex;
  align-items: center;
  gap: 0.5rem;

  svg {
    width: 2rem;
    height: 2rem;
    color: var(--primary-light);
  }
}

h1 + p {
  text-wrap: balance;
  margin-bottom: 2rem
}

/* Header */
header {
  border-bottom: 1px solid var(--border);
  background: var(--background);
  padding: .5rem 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;

  h2 {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--text-primary);
  }
}

/* Navegación del header a otras páginas */
nav {
  align-items: center;
  gap: 1rem;
  display: flex;

  a {
    text-decoration: none;
    color: var(--text-secondary);
    transition: color 0.2s;
    font-weight: 500;

    &:hover, &:focus {
      color: var(--primary);
      outline: none;
    }
  }
}

/* Botones del Header */
header div a {
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  background-color: #334155;
  color: var(--text-primary);
  display: inline-block;
  font-size: 0.875rem;
  font-weight: 700;
  text-decoration: none;
  /* TODO */
}

/* Hero */
main > section:nth-child(1) {
  height: 500px;
  text-align: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  h1 {
    padding-top: 36px;
  }

  & > img {
    position: absolute;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: -1;
    left: 0;
    right: 0;
    mask-image: linear-gradient(to bottom, rgba(16, 25, 34, 1) 5%, rgba(16, 25, 34, 0) 80%);
  }
}

/* Formulario de Búsqueda */
form {
  max-width: 42rem;
  width: 100%;
  margin: 0 auto;
  padding-inline: 1rem;
}

form > div {
  display: flex;
  align-items: center;
  background-color: var(--input-bg);
  border-radius: 0.5rem;
  box-shadow: 0 10px 15px -3px var(--shadow);
  padding: 0.5rem;
  gap: 0.5rem;
}

form span {
  padding-left: 0.75rem;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

form input[type="text"] {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text-primary);
  padding: 0.75rem 0.5rem;
  font-size: 1rem;
  font-family: inherit;
}

form input[type="text"]::placeholder {
  color: #64748b;
}

button {
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  background-color: var(--primary);
  color: white;
  font-weight: 400;
  border: none;
  cursor: pointer;
  transition: all 0.2s;
  font-family: inherit;
  font-size: 1rem;
  white-space: nowrap;

  &:hover, /* pseudo-clase que significa que el ratón está encima */
  &:focus { /* pseudo-clase que significa que el elemento tiene el foco */
    background-color: var(--primary-hover);
    outline: 2px solid white;
    outline-offset: 2px;
  }

  &:active { 
    transform: scale(0.90)
  }

  &:disabled {
    opacity: .5;
    pointer-events: none;
  }
}

/* Features Section */
main > section:nth-child(2) {
  padding-inline: 1rem;
  background-color: var(--background);
  padding-top: 2rem;

  & > header {
    gap: 2px;
    flex-direction: column;

    h2 {
      margin-bottom: 0;
    }

    p {
      opacity: .75;
    }
  }

  & > div {
    max-width: 1280px;
    margin: 0 auto;
  }
}

main > section:nth-child(2) h2 {
  font-size: 1.875rem;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 1rem;
}

main > section:nth-child(2) > div > div:first-child > p {
  font-size: 1.125rem;
  color: var(--text-muted);
  max-width: 42rem;
  margin: 0 auto;
}

/* Grid de Features */
main > section:nth-child(2) footer {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 16px
}

/* Feature Cards */
/* article {
  background-color: var(--card-bg);
  padding: 2rem;
  margin-bottom: 16px;
  border-radius: 0.5rem;                   
  box-shadow: 0px 1px 3px 0 var(--shadow); 

  svg {
    color: var(--primary-light);          
    background: rgba(0, 153, 255, 0.3);  
    border-radius: 9999px;               
    width: 64px;                         
    height: 64px;                        
    padding: 16px;                       
  }

  h3 {
    font-weight: 500;
  }

  p {
    color: var(--text-muted);
  }
} */

/* Footer */
footer {
  background-color: var(--background);
  border-top: 1px solid var(--border);
  text-align: center;
  padding: 16px;

  small {
    color: var(--text-muted);
    font-size: 0.875rem;
  }
}

article {
  background-color: var(--card-bg);
  padding: 2rem;
  margin-bottom: 16px;
  border-radius: 0.5rem;
  box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5);

  svg {
    color: var(--primary-light);
    background: rgba(0, 153, 255, .3);
    width: 64px;
    height: 64px;
    border-radius: 100%;
    padding: 16px;
  }

  p {
    color: var(--text-muted);
  }

  h3 {
    font-weight: 500;
  }
}

```

## /01-javascript/apply-button.js

```js path="/01-javascript/apply-button.js" 
const jobsListingSection = document.querySelector('.jobs-listings')

jobsListingSection.addEventListener('click', function(event) {
  const element = event.target

  if (element.classList.contains('button-apply-job')) {
    element.textContent = '¡Aplicado!'
    element.classList.add('is-applied')
    element.disabled = true
  }
})


// - Comentarios con otros eventos interesantes

// otras formas de añadir eventos click a elementos
// recupera solo el primer boton que encuentre
// const boton = document.querySelector('.button-apply-job')
// console.log(boton) // null si no lo encuentra

// if (boton !== null) {
//   boton.addEventListener('click', function() {
//     boton.textContent = '¡Aplicado!'
//     boton.classList.add('is-applied')
//     boton.disabled = true
//   })
// }

// const botones = document.querySelectorAll('.button-apply-job')
// // devuelve un NodeList (array-like) con todos los botones que encuentre
// // o una lista vacia [] si no encuentra ninguno

// botones.forEach(boton => {
//   boton.addEventListener('click', function() {
//     boton.textContent = '¡Aplicado!'
//     boton.classList.add('is-applied')
//     boton.disabled = true
//   })
// })

// ejemplos de eventos
// const searchInput = document.querySelector('#empleos-search-input')

// searchInput.addEventListener('input', function() {
//   console.log(searchInput.value)
// })

// searchInput.addEventListener('blur', function() {
//   console.log('Se dispara cuando el campo pierde el foco')
// })

// const searchForm = document.querySelector('#empleos-search-form')

// searchForm.addEventListener('submit', function(event) {
//   event.preventDefault()
//   // ... todo lo que yo te diga aqui
//   console.log('submit')
// })

// document.addEventListener('keydown', function(event) {
//   console.log("Tecla presionada: ", event.key)
//   console.log("¿Está pulsada la tecla shift?", event.shiftKey)
//   console.log("¿Está pulsada la tecla ctrl?", event.ctrlKey)
//   console.log("¿Está pulsada la tecla alt?", event.altKey)
// })
```

## /01-javascript/background.webp

Binary file available at https://raw.githubusercontent.com/midudev/jscamp/refs/heads/main/01-javascript/background.webp

## /01-javascript/config.js

```js path="/01-javascript/config.js" 
export let state = {
  count: 0
}
```

## /01-javascript/data.json

```json path="/01-javascript/data.json" 
[
  {
    "id": "7a4d1d8b-1e45-4d8c-9f1a-8c2f9a9121a4",
    "titulo": "Desarrollador de Software Senior",
    "empresa": "Tech Solutions Inc.",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un ingeniero de software con experiencia en desarrollo web y conocimientos en JavaScript, React y Node.js. El candidato ideal debe ser capaz de trabajar en equipo y tener buenas habilidades de comunicación.",
    "data": {
      "technology": ["react", "node", "javascript"],
      "modalidad": "remoto",
      "nivel": "senior"
    }
  },
  {
    "id": "d35b2c89-5d60-4f26-b19a-6cfb2f1a0f57",
    "titulo": "Analista de Datos",
    "empresa": "Data Driven Co.",
    "ubicacion": "Ciudad de México",
    "descripcion": "Estamos buscando un analista de datos con experiencia en el manejo de grandes conjuntos de datos y herramientas de visualización. Se requiere conocimiento en SQL, Python y R.",
    "data": {
      "technology": "python",
      "modalidad": "cdmx",
      "nivel": "junior"
    }
  },
  {
    "id": "e31f9a92-61d7-4b7a-b3a2-91e8c1f40b2d",
    "titulo": "Desarrollador de Aplicaciones Móviles",
    "empresa": "Mobile Apps Ltd.",
    "ubicacion": "Guadalajara",
    "descripcion": "Buscamos un desarrollador de aplicaciones móviles con experiencia en iOS y/o Android. El candidato debe tener conocimientos en Swift, Kotlin y el desarrollo de interfaces de usuario.",
    "data": {
      "technology": "mobile",
      "modalidad": "guadalajara",
      "nivel": "mid-level"
    }
  },
  {
    "id": "f62d8a34-923a-4ac2-9b0b-14e0ac2f5405",
    "titulo": "Ingeniero de DevOps",
    "empresa": "Cloud Services SA",
    "ubicacion": "Remoto",
    "descripcion": "Estamos buscando un ingeniero de DevOps con experiencia en la gestión de infraestructuras en la nube, automatización de procesos y herramientas de integración continua. Se requiere conocimiento en AWS, Azure o GCP.",
    "data": {
      "technology": "mobile",
      "modalidad": "remoto",
      "nivel": "mid-level"
    }
  },
  {
    "id": "a9f31a8e-ec38-4fd3-9114-88cc6d37a92b",
    "titulo": "Diseñador UX/UI",
    "empresa": "Creative Minds Studio",
    "ubicacion": "Barcelona",
    "descripcion": "Estamos buscando un diseñador UX/UI con pasión por crear experiencias digitales excepcionales. Se requiere experiencia en Figma, diseño centrado en el usuario y colaboración con equipos de desarrollo.",
    "data": {
      "technology": "mobile",
      "modalidad": "barcelona",
      "nivel": "mid-level"
    }
  },
  {
    "id": "c1b65b42-68c5-4f1c-a8c2-8d52c5a7a5d1",
    "titulo": "Administrador de Bases de Datos",
    "empresa": "Secure Data Corp.",
    "ubicacion": "Buenos Aires",
    "descripcion": "Se busca un administrador de bases de datos con experiencia en PostgreSQL y MySQL. El candidato deberá asegurar la disponibilidad, seguridad y rendimiento de las bases de datos de producción.",
    "data": {
      "technology": "mobile",
      "modalidad": "bsas",
      "nivel": "mid-level"
    }
  },
  {
    "id": "bb8f2a99-6a20-4f9e-912a-16f54a49b8c3",
    "titulo": "Especialista en Ciberseguridad",
    "empresa": "SafeNet Solutions",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un especialista en ciberseguridad con conocimientos en protección de infraestructuras, análisis de vulnerabilidades y respuesta ante incidentes. Se valorará experiencia con SIEM y certificaciones de seguridad.",
    "data": {
      "technology": "mobile",
      "modalidad": "remoto",
      "nivel": "mid-level"
    }
  },
  {
    "id": "fe7b2c54-4f47-4e2b-9e87-2b5413a6b24f",
    "titulo": "Product Manager",
    "empresa": "NextGen Technologies",
    "ubicacion": "Madrid",
    "descripcion": "Estamos buscando un Product Manager con experiencia en la definición y lanzamiento de productos digitales. Se requiere capacidad analítica, liderazgo y conocimiento en metodologías ágiles.",
    "data": {
      "technology": "mobile",
      "modalidad": "madrid",
      "nivel": "senior"
    }
  },
  {
    "id": "a71f7a92-56d9-4b42-9f16-cb29b40e5f2c",
    "titulo": "Frontend Developer",
    "empresa": "Bright Web Studio",
    "ubicacion": "Valencia",
    "descripcion": "Buscamos un desarrollador frontend con experiencia en React, TypeScript y Tailwind CSS. Se valorará conocimiento en optimización de rendimiento y accesibilidad web.",
    "data": {
      "technology": "react",
      "modalidad": "valencia",
      "nivel": "mid-level"
    }
  },
  {
    "id": "f91e4c7b-3840-43da-8ad7-3a52e2a8cf1d",
    "titulo": "Backend Developer",
    "empresa": "APIWorks",
    "ubicacion": "Bogotá",
    "descripcion": "Estamos buscando un desarrollador backend con experiencia en Node.js, Express y bases de datos NoSQL. Se requiere conocimiento en arquitectura de microservicios.",
    "data": {
      "technology": "node",
      "modalidad": "bogota",
      "nivel": "mid"
    }
  },
  {
    "id": "b65a3c9f-b174-4d86-b8a2-9cf9b1e13a22",
    "titulo": "Ingeniero de Machine Learning",
    "empresa": "AI Labs",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un ingeniero de machine learning con experiencia en modelos de predicción y procesamiento de datos. Se valorará conocimiento en TensorFlow, PyTorch y MLOps.",
    "data": {
      "technology": "python",
      "modalidad": "remoto",
      "nivel": "senior"
    }
  },
  {
    "id": "e8d13c45-36cb-46cf-8f9d-8a0a7b532b8b",
    "titulo": "QA Automation Engineer",
    "empresa": "Quality First",
    "ubicacion": "Lima",
    "descripcion": "Se busca ingeniero de QA con experiencia en automatización de pruebas utilizando herramientas como Selenium, Cypress o Playwright. Conocimiento deseado en CI/CD.",
    "data": {
      "technology": "mobile",
      "modalidad": "lima",
      "nivel": "mid-level"
    }
  },
  {
    "id": "d2e93b8a-0b41-4d09-9a52-36a0f418d493",
    "titulo": "Administrador de Sistemas",
    "empresa": "InfraTech Global",
    "ubicacion": "Santiago de Chile",
    "descripcion": "Buscamos un administrador de sistemas con experiencia en Linux, Docker y monitoreo de servidores. Conocimiento en scripting Bash o Python será un plus.",
    "data": {
      "technology": "mobile",
      "modalidad": "santiago",
      "nivel": "mid-level"
    }
  },
  {
    "id": "cc0c1fae-4e85-4e2c-9b02-f12f9df8a2c9",
    "titulo": "Scrum Master",
    "empresa": "Agile Minds",
    "ubicacion": "Madrid",
    "descripcion": "Estamos buscando un Scrum Master con experiencia en metodologías ágiles y gestión de equipos multidisciplinarios. Certificación Scrum Master deseable.",
    "data": {
      "technology": "mobile",
      "modalidad": "madrid",
      "nivel": "mid-level"
    }
  },
  {
    "id": "a2f1d8c6-b72c-45f5-bcc5-5c1d1f39b0b1",
    "titulo": "Soporte Técnico Nivel 2",
    "empresa": "HelpDesk Pro",
    "ubicacion": "Monterrey",
    "descripcion": "Buscamos un técnico de soporte con habilidades en resolución de incidencias, redes y sistemas operativos. Se requiere atención al detalle y excelente trato con el usuario.",
    "data": {
      "technology": "mobile",
      "modalidad": "monterrey",
      "nivel": "junior"
    }
  }
]


```

## /01-javascript/devjobs-avatar-element.js

```js path="/01-javascript/devjobs-avatar-element.js" 
class DevJobsAvatar extends HTMLElement {
  constructor() {
    super(); // llamar al constructor de HTMLElement

    this.attachShadow({ mode: 'open' })
  }

  createUrl(service, username) {
    return `https://unavatar.io/${service}/${username}`
  }

  render() {
    const service = this.getAttribute('service') ?? 'github'
    const username = this.getAttribute('username') ?? 'midudev'
    const size = this.getAttribute('size') ?? '40'

    const url = this.createUrl(service, username)

    this.shadowRoot.innerHTML = `
    <style>
      img {
        width: ${size}px;
        height: ${size}px;
        border-radius: 9999px;
      }
    </style>

      <img 
        src="${url}" 
        alt="Avatar de ${username}" 
        class="avatar"
      />
    `
  }

  connectedCallback() {
    this.render()
  }
}

customElements.define('devjobs-avatar', DevJobsAvatar)
```

## /01-javascript/empleos.html

```html path="/01-javascript/empleos.html" 
<!DOCTYPE html>
<html lang="es">

<head>
  <title>DevJobs - Empleos</title>
  <meta charset="UTF-8" />
  <meta name="description" content="Listado con empleos y filtros para encontrar el trabajo de tus sueños.">

  <link rel="stylesheet" href="./styles.css" />

  <script type="module" src="./main.js"></script>
</head>

<body>
  <header>
    <h1>
      <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
        viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <polyline points="16 18 22 12 16 6"></polyline>
        <polyline points="8 6 2 12 8 18"></polyline>
      </svg>
      DevJobs
    </h1>

    <nav>
      <!-- <a href="">
        Inicio
      </a> -->
      <a href="">Empleos</a>
    </nav>

    <div>
      <devjobs-avatar service="google" username="google.com" size="32">
      </devjobs-avatar>

      <devjobs-avatar service="google" username="netflix.com" size="32">
      </devjobs-avatar>

      <devjobs-avatar service="google" username="vercel.com" size="32">
      </devjobs-avatar>
    </div>
  </header>

  <main>
    <section class="jobs-search">
      <h1>Encuentra tu próximo trabajo</h1>
      <p>Explora miles de oportunidades en el sector tecnológico.</p>

      <form id="empleos-search-form" role="search">
        <div class="search-bar">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
            stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
            class="icon icon-tabler icons-tabler-outline icon-tabler-search">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
            <path d="M21 21l-6 -6" />
          </svg>

          <input name="search" id="empleos-search-input" required type="text"
            placeholder="Buscar trabajos, empresas o habilidades">
        </div>

        <div class="search-filters">
          <select name="technology" id="filter-technology">
            <option value="">Tecnología</option>
            <optgroup label="Tecnologías populares">
              <option value="javascript">JavaScript</option>
              <option value="python">Python</option>
              <option value="react">React</option>
              <option value="nodejs">Node.js</option>
            </optgroup>
            <option value="java">Java</option>
            <hr />
            <option value="csharp">C#</option>
            <option value="c">C</option>
            <option value="c++">C++</option>
            <hr />
            <option value="ruby">Ruby</option>
            <option value="php">PHP</option>
          </select>

          <select name="location" id="filter-location">
            <option value="">Ubicación</option>
            <option value="remoto">Remoto</option>
            <option value="cdmx">Ciudad de México</option>
            <option value="guadalajara">Guadalajara</option>
            <option value="monterrey">Monterrey</option>
            <option value="barcelona">Barcelona</option>
          </select>

          <select name="experience-level" id="filter-experience-level">
            <option value="">Nivel de experiencia</option>
            <option value="junior">Junior</option>
            <option value="mid">Mid-level</option>
            <option value="senior">Senior</option>
            <option value="lead">Lead</option>
          </select>
        </div>
      </form>

      <span id="filter-selected-value"></span>
    </section>

    <section>
      <h2>Resultados de búsqueda</h2>

      <div class="jobs-listings">
        <!-- Aquí se insertan los empleos dinámicamente -->
      </div>

      <nav class="pagination">
        <a href="#"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
            stroke-linecap="round" stroke-linejoin="round">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M15 6l-6 6l6 6" />
          </svg></a>
        <a class="is-active" href="#">1</a>
        <a href="#">2</a>
        <a href="#">3</a>
        <a href="#">4</a>
        <a href="#">5</a>
        <a href="#"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
            stroke-linecap="round" stroke-linejoin="round"
            class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M9 6l6 6l-6 6" />
          </svg></a>
      </nav>
    </section>
  </main>

  <footer style="padding-bottom: 2000px;">
    <small>&copy; 2025 DevJobs. Todos los derechos reservados.</small>
  </footer>
</body>

</html>
```

## /01-javascript/fetch-data.js

```js path="/01-javascript/fetch-data.js" 
const container = document.querySelector('.jobs-listings')

const RESULTS_PER_PAGE = 3

fetch("./data.json") /* fetch es asíncrono */
  .then((response) => {
    return response.json();
  })
  .then((jobs) => {
    jobs.forEach(job => {
      const article = document.createElement('article')
      article.className = 'job-listing-card'
      
      article.dataset.modalidad = job.data.modalidad
      article.dataset.nivel = job.data.nivel
      article.dataset.technology = job.data.technology

      article.innerHTML = `<div>
          <h3>${job.titulo}</h3>
          <small>${job.empresa} | ${job.ubicacion}</small>
          <p>${job.descripcion}</p>
        </div>
        <button class="button-apply-job">Aplicar</button>`

      container.appendChild(article)
    })
  });
```

## /01-javascript/filters.js

```js path="/01-javascript/filters.js" 
import { state } from './config.js'

state.count++

console.log(state)

const filter = document.querySelector('#filter-location')
const mensaje = document.querySelector('#filter-selected-value')

filter.addEventListener('change', function () {
  const jobs = document.querySelectorAll('.job-listing-card')

  const selectedValue = filter.value

  if (selectedValue) {
    mensaje.textContent = `Has seleccionado: ${selectedValue}`
  } else {
    mensaje.textContent = ''
  }

  jobs.forEach(job => {
    // const modalidad = job.dataset.modalidad
    const modalidad = job.getAttribute('data-modalidad')
    const isShown = selectedValue === '' || selectedValue === modalidad
    job.classList.toggle('is-hidden', isShown === false)
  })
})
```

## /01-javascript/index.html

```html path="/01-javascript/index.html" 
<!DOCTYPE html>
<html lang="es">

<head>
  <title>DevJobs - Inicio</title>
  <meta charset="UTF-8" />
  <meta name="description" content="Encuentra las mejores ofertas de trabajo para desarrolladores en DevJobs.">

  <link rel="stylesheet" href="./styles.css" />
</head>

<body>
  <header>
    <h1>
      <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
        viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <polyline points="16 18 22 12 16 6"></polyline>
        <polyline points="8 6 2 12 8 18"></polyline>
      </svg>
      DevJobs
    </h1>

    <nav>
      <!-- <a href="">
        Inicio
      </a> -->
      <a href="empleos.html">Empleos</a>
    </nav>

    <div>
      <a href="">Publicar un empleo</a>
      <!-- <a href="">Iniciar sesión</a> -->
    </div>
  </header>

  <main>
    <section>
      <img src="./background.webp" width="200" />

      <h1>Encuentra el trabajo de tus sueños</h1>

      <p>Únete a la comunidad más grande de desarrolladores y encuentra tu próxima oportunidad.</p>

      <form role="search">
        <div>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
            stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
            class="icon icon-tabler icons-tabler-outline icon-tabler-search">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
            <path d="M21 21l-6 -6" />
          </svg>

          <input required type="text" placeholder="Buscar empleos por título, habilidad o empresa">

          <button disabled type="submit">Buscar</button>
        </div>
      </form>
    </section>

    <section>

      <header>
        <h2>¿Por qué DevJobs?</h2>
        <p>DevJobs es la principal plataforma de búsqueda de empleo para desarrolladores. Conectamos a los mejores
          talentos con las empresas más innovadoras.</p>
      </header>

      <footer>
        <article>
          <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32" xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true">
            <path
              d="M216,56H176V48a24,24,0,0,0-24-24H104A24,24,0,0,0,80,48v8H40A16,16,0,0,0,24,72V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V72A16,16,0,0,0,216,56ZM96,48a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96ZM216,72v41.61A184,184,0,0,1,128,136a184.07,184.07,0,0,1-88-22.38V72Zm0,128H40V131.64A200.19,200.19,0,0,0,128,152a200.25,200.25,0,0,0,88-20.37V200ZM104,112a8,8,0,0,1,8-8h32a8,8,0,0,1,0,16H112A8,8,0,0,1,104,112Z">
            </path>
          </svg>
          <h3>Encuentra el trabajo de tus sueños</h3>
          <p>Busca miles de empleos de las mejores empresas de todo el mundo.</p>
        </article>

        <article>
          <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32" xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true">
            <path
              d="M117.25,157.92a60,60,0,1,0-66.5,0A95.83,95.83,0,0,0,3.53,195.63a8,8,0,1,0,13.4,8.74,80,80,0,0,1,134.14,0,8,8,0,0,0,13.4-8.74A95.83,95.83,0,0,0,117.25,157.92ZM40,108a44,44,0,1,1,44,44A44.05,44.05,0,0,1,40,108Zm210.14,98.7a8,8,0,0,1-11.07-2.33A79.83,79.83,0,0,0,172,168a8,8,0,0,1,0-16,44,44,0,1,0-16.34-84.87,8,8,0,1,1-5.94-14.85,60,60,0,0,1,55.53,105.64,95.83,95.83,0,0,1,47.22,37.71A8,8,0,0,1,250.14,206.7Z">
            </path>
          </svg>
          <h3>Conecta con las mejores empresas</h3>
          <p>Conecta con empresas que están contratando por tus habilidades.</p>
        </article>

        <article>
          <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32" xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true">
            <path
              d="M240,208H224V96a16,16,0,0,0-16-16H144V32a16,16,0,0,0-24.88-13.32L39.12,72A16,16,0,0,0,32,85.34V208H16a8,8,0,0,0,0,16H240a8,8,0,0,0,0-16ZM208,96V208H144V96ZM48,85.34,128,32V208H48ZM112,112v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm-32,0v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm0,56v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Zm32,0v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Z">
            </path>
          </svg>
          <h3>Obtén el salario que mereces</h3>
          <p>Obtén el salario que mereces con nuestra calculadora de salarios.</p>
        </article>
      </footer>

    </section>
  </main>

  <footer>
    <small>&copy; 2025 DevJobs. Todos los derechos reservados.</small>
  </footer>

</body>

</html>
```

## /01-javascript/main.js

```js path="/01-javascript/main.js" 
import { state } from './config.js'

import './fetch-data.js'
import './filters.js'
import './apply-button.js'
import './devjobs-avatar-element.js'

state.count++

console.log(state)
```

## /01-javascript/styles.css

```css path="/01-javascript/styles.css" 
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}






:root {
  --primary: #3b4550;
  --primary-dark: #0a3d66;
  --primary-hover: #0d5ba8;
  --primary-light: #09f;
  --background: #06182a;
  --text-primary: #ffffff;
  --text-secondary: #cbd5e1;
  --text-muted: #94a3b8;
  --border: rgba(255, 255, 255, 0.1);
  --card-bg: #1e293b;
  --shadow: rgba(0, 0, 0, 0.3);
  --input-bg: #1e293b;
  --hsla-example: hsla(210, 100%, 56%, 1);
}










@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(https://cdn.jsdelivr.net/fontsource/fonts/inter:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}










body {
  font-family: system-ui;
  background-color: var(--background);
  color: var(--text-primary);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  line-height: 1.6;
}



select {
  padding: 0.625rem 1.25rem;
  background-color: #242d3a;
  border: 0;
  border-radius: 0.5rem;
  font-size: 0.875rem;
  color: #ddd;
  cursor: pointer;
  transition: all 0.2s;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.5rem center;
  padding-right: 2.5rem;

  &:hover {
    background-color: var(--primary-hover);
    color: white;
  }

  &:focus {
    outline: 2px solid #1173d4;
    outline-offset: 2px;
  }
}









/* Títulos principales */
h1 {
  font-size: 1.5rem;
  line-height: 1.2;
  text-wrap: balance;
  display: flex;
  align-items: center;
  gap: 0.5rem;

  svg {
    width: 2rem;
    height: 2rem;
    color: var(--primary-light);
  }
}

h1 + p {
  text-wrap: balance;
  margin-bottom: 2rem
}

/* Header */
header {
  border-bottom: 1px solid var(--border);
  background: var(--background);
  padding: .5rem 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;

  h2 {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--text-primary);
  }
}

/* Navegación del header a otras páginas */
nav {
  align-items: center;
  gap: 1rem;
  display: flex;

  a {
    text-decoration: none;
    color: var(--text-secondary);
    transition: color 0.2s;
    font-weight: 500;

    &:hover, &:focus {
      color: var(--primary);
      outline: none;
    }
  }
}

/* Botones del Header */
header div a {
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  background-color: #334155;
  color: var(--text-primary);
  display: inline-block;
  font-size: 0.875rem;
  font-weight: 700;
  text-decoration: none;
  /* TODO */
}

/* Hero */
main > section:nth-child(1) {
  height: 500px;
  text-align: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  h1 {
    padding-top: 36px;
  }

  & > img {
    position: absolute;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: -1;
    left: 0;
    right: 0;
    mask-image: linear-gradient(to bottom, rgba(16, 25, 34, 1) 5%, rgba(16, 25, 34, 0) 80%);
  }
}

/* Formulario de Búsqueda */
form {
  max-width: 42rem;
  width: 100%;
  margin: 0 auto;
  padding-inline: 1rem;
}

form > div {
  display: flex;
  align-items: center;
  background-color: var(--input-bg);
  border-radius: 0.5rem;
  box-shadow: 0 10px 15px -3px var(--shadow);
  padding: 0.5rem;
  gap: 0.5rem;
}

form span {
  padding-left: 0.75rem;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

form input[type="text"] {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text-primary);
  padding: 0.75rem 0.5rem;
  font-size: 1rem;
  font-family: inherit;
}

form input[type="text"]::placeholder {
  color: #64748b;
}

button {
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  background-color: var(--primary);
  color: white;
  font-weight: 400;
  border: none;
  cursor: pointer;
  transition: all 0.2s;
  font-family: inherit;
  font-size: 1rem;
  white-space: nowrap;

  &:hover, /* pseudo-clase que significa que el ratón está encima */
  &:focus { /* pseudo-clase que significa que el elemento tiene el foco */
    background-color: var(--primary-hover);
    outline: 2px solid white;
    outline-offset: 2px;
  }

  &:active { 
    transform: scale(0.90)
  }

  &:disabled {
    opacity: .5;
    pointer-events: none;
  }
}

/* Features Section */
main > section:nth-child(2) {
  padding-inline: 1rem;
  background-color: var(--background);
  padding-top: 2rem;

  & > header {
    gap: 2px;
    flex-direction: column;

    h2 {
      margin-bottom: 0;
    }

    p {
      opacity: .75;
    }
  }

  & > div {
    max-width: 1280px;
    margin: 0 auto;
  }
}

main > section:nth-child(2) h2 {
  font-size: 1.875rem;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 1rem;
}

main > section:nth-child(2) > div > div:first-child > p {
  font-size: 1.125rem;
  color: var(--text-muted);
  max-width: 42rem;
  margin: 0 auto;
}

/* Grid de Features */
main > section:nth-child(2) footer {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 16px
}

/* Feature Cards */
/* article {
  background-color: var(--card-bg);
  padding: 2rem;
  margin-bottom: 16px;
  border-radius: 0.5rem;                   
  box-shadow: 0px 1px 3px 0 var(--shadow); 

  svg {
    color: var(--primary-light);          
    background: rgba(0, 153, 255, 0.3);  
    border-radius: 9999px;               
    width: 64px;                         
    height: 64px;                        
    padding: 16px;                       
  }

  h3 {
    font-weight: 500;
  }

  p {
    color: var(--text-muted);
  }
} */

/* Footer */
footer {
  background-color: var(--background);
  border-top: 1px solid var(--border);
  text-align: center;
  padding: 16px;

  small {
    color: var(--text-muted);
    font-size: 0.875rem;
  }
}

article {
  background-color: var(--card-bg);
  padding: 2rem;
  margin-bottom: 16px;
  border-radius: 0.5rem;
  box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5);

  svg {
    color: var(--primary-light);
    background: rgba(0, 153, 255, .3);
    width: 64px;
    height: 64px;
    border-radius: 100%;
    padding: 16px;
  }

  p {
    color: var(--text-muted);
  }

  h3 {
    font-weight: 500;
  }
}

/* Jobs Search */
.jobs-search {
  margin: 0;
  padding: 0;
  height: auto !important;

  h1 {
    font-size: 2.5rem;
    margin-bottom: .25rem;
  }

  p {
    font-size: 1.125rem;
    color: var(--text-muted);
    margin-bottom: 1.5rem;
  }

  .search-bar {
    background: var(--input-bg);
    padding: .25rem .5rem;
  }

  .search-filters {
    display: flex;
    flex-wrap: wrap;
    margin-top: .5rem;
  }

  form {
    width: 100%;
    max-width: 1280px;
  }

  form div {
    background: none;
    box-shadow: none;
    padding: 0;
  }
}

.jobs-listings {
  border: 1px solid rgba(255, 255, 255, .3);
  border-radius: 1rem;

  .job-listing-card {
    background: none;
    box-shadow: none;
    border-radius: 0;
    border-bottom: 1px solid rgba(255, 255, 255, .3);
    margin: 0;

    display: flex; /* valor original */
    align-items: start;
    gap: 1rem;

    &.is-hidden {
      display: none;
    }

    small {
      font-size: .875rem;
      opacity: .75;
    }

    p {
      margin-top: 0.5rem
    }

    &:last-child {
      border-bottom: none;
    }
  }
}

.pagination {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin-block: 2rem;

  a {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    text-decoration: none;
    color: var(--text-muted);
    border-radius: 0.375rem;
    transition: all .3s;

    &:hover, &:focus {
      background-color: #fff;
    }

    &:active {
      transform: scale(0.90);
    }

    &.is-active {
      background-color: var(--primary-light);
      color: white;
      pointer-events: none;
    }
  }
}

.button-apply-job {
  background: #09f;

  &.is-applied {
    background: #4caf50;
    pointer-events: none;
  }
}
```

## /02-react-cdn-version/apply-button.js

```js path="/02-react-cdn-version/apply-button.js" 
const jobsListingSection = document.querySelector('.jobs-listings')

jobsListingSection.addEventListener('click', function(event) {
  const element = event.target

  if (element.classList.contains('button-apply-job')) {
    element.textContent = '¡Aplicado!'
    element.classList.add('is-applied')
    element.disabled = true
  }
})


// - Comentarios con otros eventos interesantes

// otras formas de añadir eventos click a elementos
// recupera solo el primer boton que encuentre
// const boton = document.querySelector('.button-apply-job')
// console.log(boton) // null si no lo encuentra

// if (boton !== null) {
//   boton.addEventListener('click', function() {
//     boton.textContent = '¡Aplicado!'
//     boton.classList.add('is-applied')
//     boton.disabled = true
//   })
// }

// const botones = document.querySelectorAll('.button-apply-job')
// // devuelve un NodeList (array-like) con todos los botones que encuentre
// // o una lista vacia [] si no encuentra ninguno

// botones.forEach(boton => {
//   boton.addEventListener('click', function() {
//     boton.textContent = '¡Aplicado!'
//     boton.classList.add('is-applied')
//     boton.disabled = true
//   })
// })

// ejemplos de eventos
// const searchInput = document.querySelector('#empleos-search-input')

// searchInput.addEventListener('input', function() {
//   console.log(searchInput.value)
// })

// searchInput.addEventListener('blur', function() {
//   console.log('Se dispara cuando el campo pierde el foco')
// })

// const searchForm = document.querySelector('#empleos-search-form')

// searchForm.addEventListener('submit', function(event) {
//   event.preventDefault()
//   // ... todo lo que yo te diga aqui
//   console.log('submit')
// })

// document.addEventListener('keydown', function(event) {
//   console.log("Tecla presionada: ", event.key)
//   console.log("¿Está pulsada la tecla shift?", event.shiftKey)
//   console.log("¿Está pulsada la tecla ctrl?", event.ctrlKey)
//   console.log("¿Está pulsada la tecla alt?", event.altKey)
// })
```

## /02-react-cdn-version/config.js

```js path="/02-react-cdn-version/config.js" 
export let state = {
  count: 0
}
```

## /02-react-cdn-version/devjobs-avatar-element.js

```js path="/02-react-cdn-version/devjobs-avatar-element.js" 
class DevJobsAvatar extends HTMLElement {
  constructor() {
    super(); // llamar al constructor de HTMLElement

    this.attachShadow({ mode: 'open' })
  }

  createUrl(service, username) {
    return `https://unavatar.io/${service}/${username}`
  }

  render() {
    const service = this.getAttribute('service') ?? 'github'
    const username = this.getAttribute('username') ?? 'midudev'
    const size = this.getAttribute('size') ?? '40'

    const url = this.createUrl(service, username)

    this.shadowRoot.innerHTML = `
    <style>
      img {
        width: ${size}px;
        height: ${size}px;
        border-radius: 9999px;
      }
    </style>

      <img 
        src="${url}" 
        alt="Avatar de ${username}" 
        class="avatar"
      />
    `
  }

  connectedCallback() {
    this.render()
  }
}

customElements.define('devjobs-avatar', DevJobsAvatar)
```

## /02-react-cdn-version/empleos.html

```html path="/02-react-cdn-version/empleos.html" 
<!DOCTYPE html>
<html lang="es">

<head>
  <title>DevJobs - Empleos</title>
  <meta charset="UTF-8" />
  <meta name="description" content="Listado con empleos y filtros para encontrar el trabajo de tus sueños.">

  <link rel="stylesheet" href="./styles.css" />

  <script type="module" src="./main.js"></script>
</head>

<body>
  <header>
    <h1>
      <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
        viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <polyline points="16 18 22 12 16 6"></polyline>
        <polyline points="8 6 2 12 8 18"></polyline>
      </svg>
      DevJobs
    </h1>

    <nav>
      <!-- <a href="">
        Inicio
      </a> -->
      <a href="">Empleos</a>
    </nav>

    <div>
      <devjobs-avatar service="google" username="google.com" size="32">
      </devjobs-avatar>

      <devjobs-avatar service="google" username="netflix.com" size="32">
      </devjobs-avatar>

      <devjobs-avatar service="google" username="vercel.com" size="32">
      </devjobs-avatar>
    </div>
  </header>

  <main>
    <section class="jobs-search">
      <h1>Encuentra tu próximo trabajo</h1>
      <p>Explora miles de oportunidades en el sector tecnológico.</p>

      <form id="empleos-search-form" role="search">
        <div class="search-bar">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
            stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
            class="icon icon-tabler icons-tabler-outline icon-tabler-search">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
            <path d="M21 21l-6 -6" />
          </svg>

          <input name="search" id="empleos-search-input" required type="text"
            placeholder="Buscar trabajos, empresas o habilidades">
        </div>

        <div class="search-filters">
          <select name="technology" id="filter-technology">
            <option value="">Tecnología</option>
            <optgroup label="Tecnologías populares">
              <option value="javascript">JavaScript</option>
              <option value="python">Python</option>
              <option value="react">React</option>
              <option value="nodejs">Node.js</option>
            </optgroup>
            <option value="java">Java</option>
            <hr />
            <option value="csharp">C#</option>
            <option value="c">C</option>
            <option value="c++">C++</option>
            <hr />
            <option value="ruby">Ruby</option>
            <option value="php">PHP</option>
          </select>

          <select name="location" id="filter-location">
            <option value="">Ubicación</option>
            <option value="remoto">Remoto</option>
            <option value="cdmx">Ciudad de México</option>
            <option value="guadalajara">Guadalajara</option>
            <option value="monterrey">Monterrey</option>
            <option value="barcelona">Barcelona</option>
          </select>

          <select name="experience-level" id="filter-experience-level">
            <option value="">Nivel de experiencia</option>
            <option value="junior">Junior</option>
            <option value="mid">Mid-level</option>
            <option value="senior">Senior</option>
            <option value="lead">Lead</option>
          </select>
        </div>
      </form>

      <span id="filter-selected-value"></span>
    </section>

    <section>
      <h2>Resultados de búsqueda</h2>

      <div class="jobs-listings">
        <!-- Aquí se insertan los empleos dinámicamente -->
      </div>

      <nav class="pagination">
        <a href="#"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
            stroke-linecap="round" stroke-linejoin="round">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M15 6l-6 6l6 6" />
          </svg></a>
        <a class="is-active" href="#">1</a>
        <a href="#">2</a>
        <a href="#">3</a>
        <a href="#">4</a>
        <a href="#">5</a>
        <a href="#"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
            stroke-linecap="round" stroke-linejoin="round"
            class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M9 6l6 6l-6 6" />
          </svg></a>
      </nav>
    </section>
  </main>

  <footer style="padding-bottom: 2000px;">
    <small>&copy; 2025 DevJobs. Todos los derechos reservados.</small>
  </footer>
</body>

</html>
```

## /02-react-cdn-version/fetch-data.js

```js path="/02-react-cdn-version/fetch-data.js" 
const container = document.querySelector('.jobs-listings')

const RESULTS_PER_PAGE = 3

fetch("./data.json") /* fetch es asíncrono */
  .then((response) => {
    return response.json();
  })
  .then((jobs) => {
    jobs.forEach(job => {
      const article = document.createElement('article')
      article.className = 'job-listing-card'
      
      article.dataset.modalidad = job.data.modalidad
      article.dataset.nivel = job.data.nivel
      article.dataset.technology = job.data.technology

      article.innerHTML = `<div>
          <h3>${job.titulo}</h3>
          <small>${job.empresa} | ${job.ubicacion}</small>
          <p>${job.descripcion}</p>
        </div>
        <button class="button-apply-job">Aplicar</button>`

      container.appendChild(article)
    })
  });
```

## /02-react-cdn-version/filters.js

```js path="/02-react-cdn-version/filters.js" 
import { state } from './config.js'

state.count++

console.log(state)

const filter = document.querySelector('#filter-location')
const mensaje = document.querySelector('#filter-selected-value')

filter.addEventListener('change', function () {
  const jobs = document.querySelectorAll('.job-listing-card')

  const selectedValue = filter.value

  if (selectedValue) {
    mensaje.textContent = `Has seleccionado: ${selectedValue}`
  } else {
    mensaje.textContent = ''
  }

  jobs.forEach(job => {
    // const modalidad = job.dataset.modalidad
    const modalidad = job.getAttribute('data-modalidad')
    const isShown = selectedValue === '' || selectedValue === modalidad
    job.classList.toggle('is-hidden', isShown === false)
  })
})
```

## /02-react-cdn-version/index.html

```html path="/02-react-cdn-version/index.html" 
<!DOCTYPE html>
<html lang="es">

<head>
  <title>DevJobs - Inicio</title>
  <meta charset="UTF-8" />
  <meta name="description" content="Encuentra las mejores ofertas de trabajo para desarrolladores en DevJobs.">

  <link rel="stylesheet" href="./styles.css" />
</head>

<body>
  <header>
    <h1>
      <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
        viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <polyline points="16 18 22 12 16 6"></polyline>
        <polyline points="8 6 2 12 8 18"></polyline>
      </svg>
      DevJobs
    </h1>

    <nav>
      <!-- <a href="">
        Inicio
      </a> -->
      <a href="empleos.html">Empleos</a>
    </nav>

    <div>
      <a href="">Publicar un empleo</a>
      <!-- <a href="">Iniciar sesión</a> -->
    </div>
  </header>

  <main>
    <section>
      <img src="./background.webp" width="200" />

      <h1>Encuentra el trabajo de tus sueños</h1>

      <p>Únete a la comunidad más grande de desarrolladores y encuentra tu próxima oportunidad.</p>

      <form role="search">
        <div>
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
            stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"
            class="icon icon-tabler icons-tabler-outline icon-tabler-search">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
            <path d="M21 21l-6 -6" />
          </svg>

          <input required type="text" placeholder="Buscar empleos por título, habilidad o empresa">

          <button disabled type="submit">Buscar</button>
        </div>
      </form>
    </section>

    <section>

      <header>
        <h2>¿Por qué DevJobs?</h2>
        <p>DevJobs es la principal plataforma de búsqueda de empleo para desarrolladores. Conectamos a los mejores
          talentos con las empresas más innovadoras.</p>
      </header>

      <footer>
        <article>
          <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32" xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true">
            <path
              d="M216,56H176V48a24,24,0,0,0-24-24H104A24,24,0,0,0,80,48v8H40A16,16,0,0,0,24,72V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V72A16,16,0,0,0,216,56ZM96,48a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96ZM216,72v41.61A184,184,0,0,1,128,136a184.07,184.07,0,0,1-88-22.38V72Zm0,128H40V131.64A200.19,200.19,0,0,0,128,152a200.25,200.25,0,0,0,88-20.37V200ZM104,112a8,8,0,0,1,8-8h32a8,8,0,0,1,0,16H112A8,8,0,0,1,104,112Z">
            </path>
          </svg>
          <h3>Encuentra el trabajo de tus sueños</h3>
          <p>Busca miles de empleos de las mejores empresas de todo el mundo.</p>
        </article>

        <article>
          <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32" xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true">
            <path
              d="M117.25,157.92a60,60,0,1,0-66.5,0A95.83,95.83,0,0,0,3.53,195.63a8,8,0,1,0,13.4,8.74,80,80,0,0,1,134.14,0,8,8,0,0,0,13.4-8.74A95.83,95.83,0,0,0,117.25,157.92ZM40,108a44,44,0,1,1,44,44A44.05,44.05,0,0,1,40,108Zm210.14,98.7a8,8,0,0,1-11.07-2.33A79.83,79.83,0,0,0,172,168a8,8,0,0,1,0-16,44,44,0,1,0-16.34-84.87,8,8,0,1,1-5.94-14.85,60,60,0,0,1,55.53,105.64,95.83,95.83,0,0,1,47.22,37.71A8,8,0,0,1,250.14,206.7Z">
            </path>
          </svg>
          <h3>Conecta con las mejores empresas</h3>
          <p>Conecta con empresas que están contratando por tus habilidades.</p>
        </article>

        <article>
          <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32" xmlns="http://www.w3.org/2000/svg"
            aria-hidden="true">
            <path
              d="M240,208H224V96a16,16,0,0,0-16-16H144V32a16,16,0,0,0-24.88-13.32L39.12,72A16,16,0,0,0,32,85.34V208H16a8,8,0,0,0,0,16H240a8,8,0,0,0,0-16ZM208,96V208H144V96ZM48,85.34,128,32V208H48ZM112,112v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm-32,0v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm0,56v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Zm32,0v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Z">
            </path>
          </svg>
          <h3>Obtén el salario que mereces</h3>
          <p>Obtén el salario que mereces con nuestra calculadora de salarios.</p>
        </article>
      </footer>

    </section>
  </main>

  <footer>
    <small>&copy; 2025 DevJobs. Todos los derechos reservados.</small>
  </footer>

</body>

</html>
```

## /02-react-cdn-version/main.js

```js path="/02-react-cdn-version/main.js" 
import { state } from './config.js'

import './fetch-data.js'
import './filters.js'
import './apply-button.js'
import './devjobs-avatar-element.js'

state.count++

console.log(state)
```

## /02-react-cdn-version/react.html

```html path="/02-react-cdn-version/react.html" 
<!DOCTYPE html>
<html lang="es">

<head>
  <title>DevJobs - Empleos</title>
  <meta charset="UTF-8" />
  <meta name="description" content="Listado con empleos y filtros para encontrar el trabajo de tus sueños.">

  <link rel="stylesheet" href="./styles.css" />

  <script type="module">
    import React from "https://esm.sh/react?dev";
    import ReactDOM from "https://esm.sh/react-dom/client?dev";

    window.React = React
    window.ReactDOM = ReactDOM
  </script>

  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

  <script type="text/babel">
    // componente de React
    function JobCard({ titulo, empresa, ubicacion, descripcion }) {
      const [isApplied, setIsApplied] = React.useState(false)

      console.log('---> render')

      function handleClick() {
        setIsApplied(!isApplied)
      }

      const text = isApplied ? 'Aplicado' : 'Aplicar'
      const buttonClass = isApplied ? 'is-applied' : ''
      const isAppliedText = isApplied ? 'Sí' : 'No'

      return (
        <article
          className="job-listing-card"
        >
          <div>
            <h3>{titulo}</h3>
            <small>{empresa} - {ubicacion} - ¿He aplicado? {isAppliedText}</small>
            <p>{descripcion}</p>
          </div>
          <button
            className={`button-apply-job`}
            onClick={handleClick}
          >
            {text}
          </button>
        </article>
      )
    }

    function App() {
      return (
        <div className="jobs-listings">
          <JobCard
            titulo="Desarrollador/a Frontend React.js"
            empresa="Tech Solutions"
            ubicacion="Remoto"
            descripcion="Únete a nuestro equipo como desarrollador/a frontend especializado en React.js. Trabaja en proyectos innovadores y colabora con un equipo dinámico."
          />
        </div>
      )
    }

    const rootEl = document.querySelector('#root');
    const root = ReactDOM.createRoot(rootEl)
    root.render(<App />)

  </script>

</head>

<body>
  <header>
    <h1>
      <svg fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
        viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
        <polyline points="16 18 22 12 16 6"></polyline>
        <polyline points="8 6 2 12 8 18"></polyline>
      </svg>
      DevJobs
    </h1>

    <nav>
      <!-- <a href="">
        Inicio
      </a> -->
      <a href="">Empleos</a>
    </nav>

    <div>
      <devjobs-avatar service="google" username="google.com" size="32">
      </devjobs-avatar>
    </div>
  </header>

  <main>
    <section class="jobs-search">
      <h1>React.js, primeros pasos</h1>

    </section>

    <section>
      <h2>Empleos disponibles</h2>
      <div id="root"></div>
    </section>
  </main>

  <footer style="padding-bottom: 2000px;">
    <small>&copy; 2025 DevJobs. Todos los derechos reservados.</small>
  </footer>
</body>

</html>
```

## /02-react-cdn-version/styles.css

```css path="/02-react-cdn-version/styles.css" 
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}






:root {
  --primary: #3b4550;
  --primary-dark: #0a3d66;
  --primary-hover: #0d5ba8;
  --primary-light: #09f;
  --background: #06182a;
  --text-primary: #ffffff;
  --text-secondary: #cbd5e1;
  --text-muted: #94a3b8;
  --border: rgba(255, 255, 255, 0.1);
  --card-bg: #1e293b;
  --shadow: rgba(0, 0, 0, 0.3);
  --input-bg: #1e293b;
  --hsla-example: hsla(210, 100%, 56%, 1);
}










@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(https://cdn.jsdelivr.net/fontsource/fonts/inter:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}










body {
  font-family: system-ui;
  background-color: var(--background);
  color: var(--text-primary);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  line-height: 1.6;
}



select {
  padding: 0.625rem 1.25rem;
  background-color: #242d3a;
  border: 0;
  border-radius: 0.5rem;
  font-size: 0.875rem;
  color: #ddd;
  cursor: pointer;
  transition: all 0.2s;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.5rem center;
  padding-right: 2.5rem;

  &:hover {
    background-color: var(--primary-hover);
    color: white;
  }

  &:focus {
    outline: 2px solid #1173d4;
    outline-offset: 2px;
  }
}









/* Títulos principales */
h1 {
  font-size: 1.5rem;
  line-height: 1.2;
  text-wrap: balance;
  display: flex;
  align-items: center;
  gap: 0.5rem;

  svg {
    width: 2rem;
    height: 2rem;
    color: var(--primary-light);
  }
}

h1 + p {
  text-wrap: balance;
  margin-bottom: 2rem
}

/* Header */
header {
  border-bottom: 1px solid var(--border);
  background: var(--background);
  padding: .5rem 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;

  h2 {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--text-primary);
  }
}

/* Navegación del header a otras páginas */
nav {
  align-items: center;
  gap: 1rem;
  display: flex;

  a {
    text-decoration: none;
    color: var(--text-secondary);
    transition: color 0.2s;
    font-weight: 500;

    &:hover, &:focus {
      color: var(--primary);
      outline: none;
    }
  }
}

/* Botones del Header */
header div a {
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  background-color: #334155;
  color: var(--text-primary);
  display: inline-block;
  font-size: 0.875rem;
  font-weight: 700;
  text-decoration: none;
  /* TODO */
}

/* Hero */
main > section:nth-child(1) {
  height: 500px;
  text-align: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  h1 {
    padding-top: 36px;
  }

  & > img {
    position: absolute;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: -1;
    left: 0;
    right: 0;
    mask-image: linear-gradient(to bottom, rgba(16, 25, 34, 1) 5%, rgba(16, 25, 34, 0) 80%);
  }
}

/* Formulario de Búsqueda */
form {
  max-width: 42rem;
  width: 100%;
  margin: 0 auto;
  padding-inline: 1rem;
}

form > div {
  display: flex;
  align-items: center;
  background-color: var(--input-bg);
  border-radius: 0.5rem;
  box-shadow: 0 10px 15px -3px var(--shadow);
  padding: 0.5rem;
  gap: 0.5rem;
}

form span {
  padding-left: 0.75rem;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

form input[type="text"] {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text-primary);
  padding: 0.75rem 0.5rem;
  font-size: 1rem;
  font-family: inherit;
}

form input[type="text"]::placeholder {
  color: #64748b;
}

button {
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  background-color: var(--primary);
  color: white;
  font-weight: 400;
  border: none;
  cursor: pointer;
  transition: all 0.2s;
  font-family: inherit;
  font-size: 1rem;
  white-space: nowrap;

  &:hover, /* pseudo-clase que significa que el ratón está encima */
  &:focus { /* pseudo-clase que significa que el elemento tiene el foco */
    background-color: var(--primary-hover);
    outline: 2px solid white;
    outline-offset: 2px;
  }

  &:active { 
    transform: scale(0.90)
  }

  &:disabled {
    opacity: .5;
    pointer-events: none;
  }
}

/* Features Section */
main > section:nth-child(2) {
  padding-inline: 1rem;
  background-color: var(--background);
  padding-top: 2rem;

  & > header {
    gap: 2px;
    flex-direction: column;

    h2 {
      margin-bottom: 0;
    }

    p {
      opacity: .75;
    }
  }

  & > div {
    max-width: 1280px;
    margin: 0 auto;
  }
}

main > section:nth-child(2) h2 {
  font-size: 1.875rem;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 1rem;
}

main > section:nth-child(2) > div > div:first-child > p {
  font-size: 1.125rem;
  color: var(--text-muted);
  max-width: 42rem;
  margin: 0 auto;
}

/* Grid de Features */
main > section:nth-child(2) footer {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 16px
}

/* Feature Cards */
/* article {
  background-color: var(--card-bg);
  padding: 2rem;
  margin-bottom: 16px;
  border-radius: 0.5rem;                   
  box-shadow: 0px 1px 3px 0 var(--shadow); 

  svg {
    color: var(--primary-light);          
    background: rgba(0, 153, 255, 0.3);  
    border-radius: 9999px;               
    width: 64px;                         
    height: 64px;                        
    padding: 16px;                       
  }

  h3 {
    font-weight: 500;
  }

  p {
    color: var(--text-muted);
  }
} */

/* Footer */
footer {
  background-color: var(--background);
  border-top: 1px solid var(--border);
  text-align: center;
  padding: 16px;

  small {
    color: var(--text-muted);
    font-size: 0.875rem;
  }
}

article {
  background-color: var(--card-bg);
  padding: 2rem;
  margin-bottom: 16px;
  border-radius: 0.5rem;
  box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5);

  svg {
    color: var(--primary-light);
    background: rgba(0, 153, 255, .3);
    width: 64px;
    height: 64px;
    border-radius: 100%;
    padding: 16px;
  }

  p {
    color: var(--text-muted);
  }

  h3 {
    font-weight: 500;
  }
}

/* Jobs Search */
.jobs-search {
  margin: 0;
  padding: 0;
  height: auto !important;

  h1 {
    font-size: 2.5rem;
    margin-bottom: .25rem;
  }

  p {
    font-size: 1.125rem;
    color: var(--text-muted);
    margin-bottom: 1.5rem;
  }

  .search-bar {
    background: var(--input-bg);
    padding: .25rem .5rem;
  }

  .search-filters {
    display: flex;
    flex-wrap: wrap;
    margin-top: .5rem;
  }

  form {
    width: 100%;
    max-width: 1280px;
  }

  form div {
    background: none;
    box-shadow: none;
    padding: 0;
  }
}

.jobs-listings {
  border: 1px solid rgba(255, 255, 255, .3);
  border-radius: 1rem;

  .job-listing-card {
    background: none;
    box-shadow: none;
    border-radius: 0;
    border-bottom: 1px solid rgba(255, 255, 255, .3);
    margin: 0;

    display: flex; /* valor original */
    align-items: start;
    gap: 1rem;

    &.is-hidden {
      display: none;
    }

    small {
      font-size: .875rem;
      opacity: .75;
    }

    p {
      margin-top: 0.5rem
    }

    &:last-child {
      border-bottom: none;
    }
  }
}

.pagination {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin-block: 2rem;

  a {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    text-decoration: none;
    color: var(--text-muted);
    border-radius: 0.375rem;
    transition: all .3s;

    &:hover, &:focus {
      background-color: #fff;
    }

    &:active {
      transform: scale(0.90);
    }

    &.is-active {
      background-color: var(--primary-light);
      color: white;
      pointer-events: none;
    }
  }
}

.button-apply-job {
  background: #09f;

  &.is-applied {
    background: #4caf50;
    pointer-events: none;
  }
}
```

## /02-react/.gitignore

```gitignore path="/02-react/.gitignore" 
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

```

## /02-react/README.md

# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## React Compiler

The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.

## Expanding the ESLint configuration

If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.


## /02-react/eslint.config.js

```js path="/02-react/eslint.config.js" 
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{js,jsx}'],
    extends: [
      js.configs.recommended,
      reactHooks.configs['recommended-latest'],
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: { jsx: true },
        sourceType: 'module',
      },
    },
    rules: {
      'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
    },
  },
])

```

## /02-react/index.html

```html path="/02-react/index.html" 
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>

</html>
```

## /02-react/package-lock.json

```json path="/02-react/package-lock.json" 
{
  "name": "02-react",
  "version": "0.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "02-react",
      "version": "0.0.0",
      "dependencies": {
        "react": "^19.1.1",
        "react-dom": "^19.1.1"
      },
      "devDependencies": {
        "@eslint/js": "^9.36.0",
        "@types/react": "^19.1.16",
        "@types/react-dom": "^19.1.9",
        "@vitejs/plugin-react-swc": "^4.1.0",
        "eslint": "^9.36.0",
        "eslint-plugin-react-hooks": "^5.2.0",
        "eslint-plugin-react-refresh": "^0.4.22",
        "globals": "^16.4.0",
        "vite": "^7.1.7"
      }
    },
    "node_modules/@esbuild/aix-ppc64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
      "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==",
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "aix"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz",
      "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==",
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz",
      "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-x64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz",
      "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/darwin-arm64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
      "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/darwin-x64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz",
      "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-arm64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz",
      "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-x64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz",
      "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz",
      "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==",
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz",
      "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ia32": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz",
      "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==",
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-loong64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz",
      "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==",
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-mips64el": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz",
      "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==",
      "cpu": [
        "mips64el"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ppc64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz",
      "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==",
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-riscv64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz",
      "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==",
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-s390x": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz",
      "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==",
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-x64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz",
      "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-arm64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz",
      "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-x64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz",
      "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-arm64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz",
      "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-x64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz",
      "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openharmony-arm64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz",
      "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/sunos-x64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz",
      "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "sunos"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-arm64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz",
      "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-ia32": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz",
      "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==",
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/win32-x64": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz",
      "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@eslint-community/eslint-utils": {
      "version": "4.9.0",
      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
      "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "eslint-visitor-keys": "^3.4.3"
      },
      "engines": {
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      },
      "peerDependencies": {
        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
      }
    },
    "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
      "version": "3.4.3",
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/@eslint-community/regexpp": {
      "version": "4.12.2",
      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
      "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
      }
    },
    "node_modules/@eslint/config-array": {
      "version": "0.21.1",
      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
      "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "@eslint/object-schema": "^2.1.7",
        "debug": "^4.3.1",
        "minimatch": "^3.1.2"
      },
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      }
    },
    "node_modules/@eslint/config-helpers": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
      "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "@eslint/core": "^0.16.0"
      },
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      }
    },
    "node_modules/@eslint/core": {
      "version": "0.16.0",
      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
      "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "@types/json-schema": "^7.0.15"
      },
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      }
    },
    "node_modules/@eslint/eslintrc": {
      "version": "3.3.1",
      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
      "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ajv": "^6.12.4",
        "debug": "^4.3.2",
        "espree": "^10.0.1",
        "globals": "^14.0.0",
        "ignore": "^5.2.0",
        "import-fresh": "^3.2.1",
        "js-yaml": "^4.1.0",
        "minimatch": "^3.1.2",
        "strip-json-comments": "^3.1.1"
      },
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/@eslint/eslintrc/node_modules/globals": {
      "version": "14.0.0",
      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/@eslint/js": {
      "version": "9.38.0",
      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
      "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      },
      "funding": {
        "url": "https://eslint.org/donate"
      }
    },
    "node_modules/@eslint/object-schema": {
      "version": "2.1.7",
      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
      "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      }
    },
    "node_modules/@eslint/plugin-kit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
      "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "@eslint/core": "^0.16.0",
        "levn": "^0.4.1"
      },
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      }
    },
    "node_modules/@humanfs/core": {
      "version": "0.19.1",
      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": ">=18.18.0"
      }
    },
    "node_modules/@humanfs/node": {
      "version": "0.16.7",
      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
      "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "@humanfs/core": "^0.19.1",
        "@humanwhocodes/retry": "^0.4.0"
      },
      "engines": {
        "node": ">=18.18.0"
      }
    },
    "node_modules/@humanwhocodes/module-importer": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": ">=12.22"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/nzakas"
      }
    },
    "node_modules/@humanwhocodes/retry": {
      "version": "0.4.3",
      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
      "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": ">=18.18"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/nzakas"
      }
    },
    "node_modules/@rolldown/pluginutils": {
      "version": "1.0.0-beta.35",
      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz",
      "integrity": "sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@rollup/rollup-android-arm-eabi": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
      "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ]
    },
    "node_modules/@rollup/rollup-android-arm64": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
      "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ]
    },
    "node_modules/@rollup/rollup-darwin-arm64": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
      "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ]
    },
    "node_modules/@rollup/rollup-darwin-x64": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
      "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ]
    },
    "node_modules/@rollup/rollup-freebsd-arm64": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
      "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ]
    },
    "node_modules/@rollup/rollup-freebsd-x64": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
      "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
      "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
      "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm64-gnu": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
      "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-arm64-musl": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
      "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-loong64-gnu": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
      "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
      "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
      "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-riscv64-musl": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
      "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-s390x-gnu": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
      "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-x64-gnu": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
      "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-linux-x64-musl": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
      "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ]
    },
    "node_modules/@rollup/rollup-openharmony-arm64": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
      "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ]
    },
    "node_modules/@rollup/rollup-win32-arm64-msvc": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
      "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-ia32-msvc": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
      "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-x64-gnu": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
      "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@rollup/rollup-win32-x64-msvc": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
      "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "win32"
      ]
    },
    "node_modules/@swc/core": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.5.tgz",
      "integrity": "sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==",
      "dev": true,
      "hasInstallScript": true,
      "license": "Apache-2.0",
      "dependencies": {
        "@swc/counter": "^0.1.3",
        "@swc/types": "^0.1.24"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/swc"
      },
      "optionalDependencies": {
        "@swc/core-darwin-arm64": "1.13.5",
        "@swc/core-darwin-x64": "1.13.5",
        "@swc/core-linux-arm-gnueabihf": "1.13.5",
        "@swc/core-linux-arm64-gnu": "1.13.5",
        "@swc/core-linux-arm64-musl": "1.13.5",
        "@swc/core-linux-x64-gnu": "1.13.5",
        "@swc/core-linux-x64-musl": "1.13.5",
        "@swc/core-win32-arm64-msvc": "1.13.5",
        "@swc/core-win32-ia32-msvc": "1.13.5",
        "@swc/core-win32-x64-msvc": "1.13.5"
      },
      "peerDependencies": {
        "@swc/helpers": ">=0.5.17"
      },
      "peerDependenciesMeta": {
        "@swc/helpers": {
          "optional": true
        }
      }
    },
    "node_modules/@swc/core-darwin-arm64": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz",
      "integrity": "sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "Apache-2.0 AND MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/core-darwin-x64": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz",
      "integrity": "sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "Apache-2.0 AND MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/core-linux-arm-gnueabihf": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz",
      "integrity": "sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==",
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "Apache-2.0",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/core-linux-arm64-gnu": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz",
      "integrity": "sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "Apache-2.0 AND MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/core-linux-arm64-musl": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz",
      "integrity": "sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "Apache-2.0 AND MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/core-linux-x64-gnu": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz",
      "integrity": "sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "Apache-2.0 AND MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/core-linux-x64-musl": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz",
      "integrity": "sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "Apache-2.0 AND MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/core-win32-arm64-msvc": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz",
      "integrity": "sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==",
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "Apache-2.0 AND MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/core-win32-ia32-msvc": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz",
      "integrity": "sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==",
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "Apache-2.0 AND MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/core-win32-x64-msvc": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz",
      "integrity": "sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==",
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "Apache-2.0 AND MIT",
      "optional": true,
      "os": [
        "win32"
      ],
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/@swc/counter": {
      "version": "0.1.3",
      "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
      "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
      "dev": true,
      "license": "Apache-2.0"
    },
    "node_modules/@swc/types": {
      "version": "0.1.25",
      "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz",
      "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==",
      "dev": true,
      "license": "Apache-2.0",
      "dependencies": {
        "@swc/counter": "^0.1.3"
      }
    },
    "node_modules/@types/estree": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/json-schema": {
      "version": "7.0.15",
      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/@types/react": {
      "version": "19.2.2",
      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
      "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "csstype": "^3.0.2"
      }
    },
    "node_modules/@types/react-dom": {
      "version": "19.2.2",
      "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
      "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
      "dev": true,
      "license": "MIT",
      "peerDependencies": {
        "@types/react": "^19.2.0"
      }
    },
    "node_modules/@vitejs/plugin-react-swc": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz",
      "integrity": "sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@rolldown/pluginutils": "1.0.0-beta.35",
        "@swc/core": "^1.13.5"
      },
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      },
      "peerDependencies": {
        "vite": "^4 || ^5 || ^6 || ^7"
      }
    },
    "node_modules/acorn": {
      "version": "8.15.0",
      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "bin": {
        "acorn": "bin/acorn"
      },
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/acorn-jsx": {
      "version": "5.3.2",
      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
      "dev": true,
      "license": "MIT",
      "peerDependencies": {
        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
      }
    },
    "node_modules/ajv": {
      "version": "6.12.6",
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "fast-deep-equal": "^3.1.1",
        "fast-json-stable-stringify": "^2.0.0",
        "json-schema-traverse": "^0.4.1",
        "uri-js": "^4.2.2"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/epoberezkin"
      }
    },
    "node_modules/ansi-styles": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "color-convert": "^2.0.1"
      },
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
      }
    },
    "node_modules/argparse": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
      "dev": true,
      "license": "Python-2.0"
    },
    "node_modules/balanced-match": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/brace-expansion": {
      "version": "1.1.12",
      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "balanced-match": "^1.0.0",
        "concat-map": "0.0.1"
      }
    },
    "node_modules/callsites": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/chalk": {
      "version": "4.1.2",
      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ansi-styles": "^4.1.0",
        "supports-color": "^7.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/chalk/chalk?sponsor=1"
      }
    },
    "node_modules/color-convert": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "color-name": "~1.1.4"
      },
      "engines": {
        "node": ">=7.0.0"
      }
    },
    "node_modules/color-name": {
      "version": "1.1.4",
      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/concat-map": {
      "version": "0.0.1",
      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/cross-spawn": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.1.0",
        "shebang-command": "^2.0.0",
        "which": "^2.0.1"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/csstype": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
      "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/deep-is": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/esbuild": {
      "version": "0.25.11",
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
      "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==",
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "bin": {
        "esbuild": "bin/esbuild"
      },
      "engines": {
        "node": ">=18"
      },
      "optionalDependencies": {
        "@esbuild/aix-ppc64": "0.25.11",
        "@esbuild/android-arm": "0.25.11",
        "@esbuild/android-arm64": "0.25.11",
        "@esbuild/android-x64": "0.25.11",
        "@esbuild/darwin-arm64": "0.25.11",
        "@esbuild/darwin-x64": "0.25.11",
        "@esbuild/freebsd-arm64": "0.25.11",
        "@esbuild/freebsd-x64": "0.25.11",
        "@esbuild/linux-arm": "0.25.11",
        "@esbuild/linux-arm64": "0.25.11",
        "@esbuild/linux-ia32": "0.25.11",
        "@esbuild/linux-loong64": "0.25.11",
        "@esbuild/linux-mips64el": "0.25.11",
        "@esbuild/linux-ppc64": "0.25.11",
        "@esbuild/linux-riscv64": "0.25.11",
        "@esbuild/linux-s390x": "0.25.11",
        "@esbuild/linux-x64": "0.25.11",
        "@esbuild/netbsd-arm64": "0.25.11",
        "@esbuild/netbsd-x64": "0.25.11",
        "@esbuild/openbsd-arm64": "0.25.11",
        "@esbuild/openbsd-x64": "0.25.11",
        "@esbuild/openharmony-arm64": "0.25.11",
        "@esbuild/sunos-x64": "0.25.11",
        "@esbuild/win32-arm64": "0.25.11",
        "@esbuild/win32-ia32": "0.25.11",
        "@esbuild/win32-x64": "0.25.11"
      }
    },
    "node_modules/escape-string-regexp": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/eslint": {
      "version": "9.38.0",
      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
      "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "@eslint-community/eslint-utils": "^4.8.0",
        "@eslint-community/regexpp": "^4.12.1",
        "@eslint/config-array": "^0.21.1",
        "@eslint/config-helpers": "^0.4.1",
        "@eslint/core": "^0.16.0",
        "@eslint/eslintrc": "^3.3.1",
        "@eslint/js": "9.38.0",
        "@eslint/plugin-kit": "^0.4.0",
        "@humanfs/node": "^0.16.6",
        "@humanwhocodes/module-importer": "^1.0.1",
        "@humanwhocodes/retry": "^0.4.2",
        "@types/estree": "^1.0.6",
        "ajv": "^6.12.4",
        "chalk": "^4.0.0",
        "cross-spawn": "^7.0.6",
        "debug": "^4.3.2",
        "escape-string-regexp": "^4.0.0",
        "eslint-scope": "^8.4.0",
        "eslint-visitor-keys": "^4.2.1",
        "espree": "^10.4.0",
        "esquery": "^1.5.0",
        "esutils": "^2.0.2",
        "fast-deep-equal": "^3.1.3",
        "file-entry-cache": "^8.0.0",
        "find-up": "^5.0.0",
        "glob-parent": "^6.0.2",
        "ignore": "^5.2.0",
        "imurmurhash": "^0.1.4",
        "is-glob": "^4.0.0",
        "json-stable-stringify-without-jsonify": "^1.0.1",
        "lodash.merge": "^4.6.2",
        "minimatch": "^3.1.2",
        "natural-compare": "^1.4.0",
        "optionator": "^0.9.3"
      },
      "bin": {
        "eslint": "bin/eslint.js"
      },
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      },
      "funding": {
        "url": "https://eslint.org/donate"
      },
      "peerDependencies": {
        "jiti": "*"
      },
      "peerDependenciesMeta": {
        "jiti": {
          "optional": true
        }
      }
    },
    "node_modules/eslint-plugin-react-hooks": {
      "version": "5.2.0",
      "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz",
      "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "peerDependencies": {
        "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
      }
    },
    "node_modules/eslint-plugin-react-refresh": {
      "version": "0.4.24",
      "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
      "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
      "dev": true,
      "license": "MIT",
      "peerDependencies": {
        "eslint": ">=8.40"
      }
    },
    "node_modules/eslint-scope": {
      "version": "8.4.0",
      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
      "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
      "dev": true,
      "license": "BSD-2-Clause",
      "dependencies": {
        "esrecurse": "^4.3.0",
        "estraverse": "^5.2.0"
      },
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/eslint-visitor-keys": {
      "version": "4.2.1",
      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
      "dev": true,
      "license": "Apache-2.0",
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/espree": {
      "version": "10.4.0",
      "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
      "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
      "dev": true,
      "license": "BSD-2-Clause",
      "dependencies": {
        "acorn": "^8.15.0",
        "acorn-jsx": "^5.3.2",
        "eslint-visitor-keys": "^4.2.1"
      },
      "engines": {
        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
      },
      "funding": {
        "url": "https://opencollective.com/eslint"
      }
    },
    "node_modules/esquery": {
      "version": "1.6.0",
      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
      "dev": true,
      "license": "BSD-3-Clause",
      "dependencies": {
        "estraverse": "^5.1.0"
      },
      "engines": {
        "node": ">=0.10"
      }
    },
    "node_modules/esrecurse": {
      "version": "4.3.0",
      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
      "dev": true,
      "license": "BSD-2-Clause",
      "dependencies": {
        "estraverse": "^5.2.0"
      },
      "engines": {
        "node": ">=4.0"
      }
    },
    "node_modules/estraverse": {
      "version": "5.3.0",
      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
      "dev": true,
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=4.0"
      }
    },
    "node_modules/esutils": {
      "version": "2.0.3",
      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
      "dev": true,
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/fast-json-stable-stringify": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/fast-levenshtein": {
      "version": "2.0.6",
      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/fdir": {
      "version": "6.5.0",
      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=12.0.0"
      },
      "peerDependencies": {
        "picomatch": "^3 || ^4"
      },
      "peerDependenciesMeta": {
        "picomatch": {
          "optional": true
        }
      }
    },
    "node_modules/file-entry-cache": {
      "version": "8.0.0",
      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "flat-cache": "^4.0.0"
      },
      "engines": {
        "node": ">=16.0.0"
      }
    },
    "node_modules/find-up": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "locate-path": "^6.0.0",
        "path-exists": "^4.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/flat-cache": {
      "version": "4.0.1",
      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "flatted": "^3.2.9",
        "keyv": "^4.5.4"
      },
      "engines": {
        "node": ">=16"
      }
    },
    "node_modules/flatted": {
      "version": "3.3.3",
      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
      "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "darwin"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/glob-parent": {
      "version": "6.0.2",
      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "is-glob": "^4.0.3"
      },
      "engines": {
        "node": ">=10.13.0"
      }
    },
    "node_modules/globals": {
      "version": "16.4.0",
      "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
      "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/has-flag": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/ignore": {
      "version": "5.3.2",
      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 4"
      }
    },
    "node_modules/import-fresh": {
      "version": "3.3.1",
      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "parent-module": "^1.0.0",
        "resolve-from": "^4.0.0"
      },
      "engines": {
        "node": ">=6"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/imurmurhash": {
      "version": "0.1.4",
      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.8.19"
      }
    },
    "node_modules/is-extglob": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/is-glob": {
      "version": "4.0.3",
      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "is-extglob": "^2.1.1"
      },
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/js-yaml": {
      "version": "4.1.0",
      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "argparse": "^2.0.1"
      },
      "bin": {
        "js-yaml": "bin/js-yaml.js"
      }
    },
    "node_modules/json-buffer": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/json-schema-traverse": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/json-stable-stringify-without-jsonify": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/keyv": {
      "version": "4.5.4",
      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "json-buffer": "3.0.1"
      }
    },
    "node_modules/levn": {
      "version": "0.4.1",
      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "prelude-ls": "^1.2.1",
        "type-check": "~0.4.0"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/locate-path": {
      "version": "6.0.0",
      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "p-locate": "^5.0.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/lodash.merge": {
      "version": "4.6.2",
      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/minimatch": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "brace-expansion": "^1.1.7"
      },
      "engines": {
        "node": "*"
      }
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/nanoid": {
      "version": "3.3.11",
      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
      "dev": true,
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "bin": {
        "nanoid": "bin/nanoid.cjs"
      },
      "engines": {
        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
      }
    },
    "node_modules/natural-compare": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
      "dev": true,
      "license": "MIT"
    },
    "node_modules/optionator": {
      "version": "0.9.4",
      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "deep-is": "^0.1.3",
        "fast-levenshtein": "^2.0.6",
        "levn": "^0.4.1",
        "prelude-ls": "^1.2.1",
        "type-check": "^0.4.0",
        "word-wrap": "^1.2.5"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/p-limit": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "yocto-queue": "^0.1.0"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/p-locate": {
      "version": "5.0.0",
      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "p-limit": "^3.0.2"
      },
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/parent-module": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "callsites": "^3.0.0"
      },
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/path-exists": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-key": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/picocolors": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
      "dev": true,
      "license": "ISC"
    },
    "node_modules/picomatch": {
      "version": "4.0.3",
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://github.com/sponsors/jonschlinkert"
      }
    },
    "node_modules/postcss": {
      "version": "8.5.6",
      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
      "dev": true,
      "funding": [
        {
          "type": "opencollective",
          "url": "https://opencollective.com/postcss/"
        },
        {
          "type": "tidelift",
          "url": "https://tidelift.com/funding/github/npm/postcss"
        },
        {
          "type": "github",
          "url": "https://github.com/sponsors/ai"
        }
      ],
      "license": "MIT",
      "dependencies": {
        "nanoid": "^3.3.11",
        "picocolors": "^1.1.1",
        "source-map-js": "^1.2.1"
      },
      "engines": {
        "node": "^10 || ^12 || >=14"
      }
    },
    "node_modules/prelude-ls": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/punycode": {
      "version": "2.3.1",
      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/react": {
      "version": "19.2.0",
      "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
      "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
      "license": "MIT",
      "peer": true,
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/react-dom": {
      "version": "19.2.0",
      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
      "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
      "license": "MIT",
      "dependencies": {
        "scheduler": "^0.27.0"
      },
      "peerDependencies": {
        "react": "^19.2.0"
      }
    },
    "node_modules/resolve-from": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=4"
      }
    },
    "node_modules/rollup": {
      "version": "4.52.5",
      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
      "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "@types/estree": "1.0.8"
      },
      "bin": {
        "rollup": "dist/bin/rollup"
      },
      "engines": {
        "node": ">=18.0.0",
        "npm": ">=8.0.0"
      },
      "optionalDependencies": {
        "@rollup/rollup-android-arm-eabi": "4.52.5",
        "@rollup/rollup-android-arm64": "4.52.5",
        "@rollup/rollup-darwin-arm64": "4.52.5",
        "@rollup/rollup-darwin-x64": "4.52.5",
        "@rollup/rollup-freebsd-arm64": "4.52.5",
        "@rollup/rollup-freebsd-x64": "4.52.5",
        "@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
        "@rollup/rollup-linux-arm-musleabihf": "4.52.5",
        "@rollup/rollup-linux-arm64-gnu": "4.52.5",
        "@rollup/rollup-linux-arm64-musl": "4.52.5",
        "@rollup/rollup-linux-loong64-gnu": "4.52.5",
        "@rollup/rollup-linux-ppc64-gnu": "4.52.5",
        "@rollup/rollup-linux-riscv64-gnu": "4.52.5",
        "@rollup/rollup-linux-riscv64-musl": "4.52.5",
        "@rollup/rollup-linux-s390x-gnu": "4.52.5",
        "@rollup/rollup-linux-x64-gnu": "4.52.5",
        "@rollup/rollup-linux-x64-musl": "4.52.5",
        "@rollup/rollup-openharmony-arm64": "4.52.5",
        "@rollup/rollup-win32-arm64-msvc": "4.52.5",
        "@rollup/rollup-win32-ia32-msvc": "4.52.5",
        "@rollup/rollup-win32-x64-gnu": "4.52.5",
        "@rollup/rollup-win32-x64-msvc": "4.52.5",
        "fsevents": "~2.3.2"
      }
    },
    "node_modules/scheduler": {
      "version": "0.27.0",
      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
      "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
      "license": "MIT"
    },
    "node_modules/shebang-command": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "shebang-regex": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/shebang-regex": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/source-map-js": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
      "dev": true,
      "license": "BSD-3-Clause",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/strip-json-comments": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=8"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/supports-color": {
      "version": "7.2.0",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "has-flag": "^4.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/tinyglobby": {
      "version": "0.2.15",
      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "fdir": "^6.5.0",
        "picomatch": "^4.0.3"
      },
      "engines": {
        "node": ">=12.0.0"
      },
      "funding": {
        "url": "https://github.com/sponsors/SuperchupuDev"
      }
    },
    "node_modules/type-check": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "prelude-ls": "^1.2.1"
      },
      "engines": {
        "node": ">= 0.8.0"
      }
    },
    "node_modules/uri-js": {
      "version": "4.4.1",
      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
      "dev": true,
      "license": "BSD-2-Clause",
      "dependencies": {
        "punycode": "^2.1.0"
      }
    },
    "node_modules/vite": {
      "version": "7.1.11",
      "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
      "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
      "dev": true,
      "license": "MIT",
      "peer": true,
      "dependencies": {
        "esbuild": "^0.25.0",
        "fdir": "^6.5.0",
        "picomatch": "^4.0.3",
        "postcss": "^8.5.6",
        "rollup": "^4.43.0",
        "tinyglobby": "^0.2.15"
      },
      "bin": {
        "vite": "bin/vite.js"
      },
      "engines": {
        "node": "^20.19.0 || >=22.12.0"
      },
      "funding": {
        "url": "https://github.com/vitejs/vite?sponsor=1"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.3"
      },
      "peerDependencies": {
        "@types/node": "^20.19.0 || >=22.12.0",
        "jiti": ">=1.21.0",
        "less": "^4.0.0",
        "lightningcss": "^1.21.0",
        "sass": "^1.70.0",
        "sass-embedded": "^1.70.0",
        "stylus": ">=0.54.8",
        "sugarss": "^5.0.0",
        "terser": "^5.16.0",
        "tsx": "^4.8.1",
        "yaml": "^2.4.2"
      },
      "peerDependenciesMeta": {
        "@types/node": {
          "optional": true
        },
        "jiti": {
          "optional": true
        },
        "less": {
          "optional": true
        },
        "lightningcss": {
          "optional": true
        },
        "sass": {
          "optional": true
        },
        "sass-embedded": {
          "optional": true
        },
        "stylus": {
          "optional": true
        },
        "sugarss": {
          "optional": true
        },
        "terser": {
          "optional": true
        },
        "tsx": {
          "optional": true
        },
        "yaml": {
          "optional": true
        }
      }
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
      "dev": true,
      "license": "ISC",
      "dependencies": {
        "isexe": "^2.0.0"
      },
      "bin": {
        "node-which": "bin/node-which"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/word-wrap": {
      "version": "1.2.5",
      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/yocto-queue": {
      "version": "0.1.0",
      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
      "dev": true,
      "license": "MIT",
      "engines": {
        "node": ">=10"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    }
  }
}

```

## /02-react/package.json

```json path="/02-react/package.json" 
{
  "name": "02-react",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.1",
    "react-dom": "^19.1.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react-swc": "^4.1.0",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "vite": "^7.1.7"
  }
}

```

## /02-react/public/background.webp

Binary file available at https://raw.githubusercontent.com/midudev/jscamp/refs/heads/main/02-react/public/background.webp

## /02-react/public/vite.svg

```svg path="/02-react/public/vite.svg" 
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
```

## /02-react/src/App.jsx

```jsx path="/02-react/src/App.jsx" 
import { Header } from './components/Header.jsx'
import { Footer } from './components/Footer.jsx'

import { HomePage } from './pages/Home.jsx'
import { SearchPage } from './pages/Search.jsx'
import { Route } from './components/Route.jsx'

function App() {
  return (
    <>
      <Header />
      <Route path="/" component={HomePage} />
      <Route path="/search" component={SearchPage} />
      <Footer />
    </>
  )
}

export default App

```

## /02-react/src/assets/react.svg

```svg path="/02-react/src/assets/react.svg" 
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
```

## /02-react/src/components/Footer.jsx

```jsx path="/02-react/src/components/Footer.jsx" 
export function Footer () {
  return (
    <footer>
      <small>&copy; 2025 DevJobs. Todos los derechos reservados.</small>
    </footer>
  )
}
```

## /02-react/src/components/Header.jsx

```jsx path="/02-react/src/components/Header.jsx" 
import { Link } from "./Link";

export function Header () {
  return (
    <header>
      <Link href='/' style={{ textDecoration: 'none' }}>
        <h1 style={{ color: 'white' }}>
            <svg fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
              viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
              <polyline points="16 18 22 12 16 6"></polyline>
              <polyline points="8 6 2 12 8 18"></polyline>
            </svg>
            DevJobs
        </h1>
      </Link>

      <nav>
        <Link href="/search">Empleos</Link>

        <a href='/search'>Sin SPA</a>
      </nav>

    </header>
  )
}
```

## /02-react/src/components/JobCard.jsx

```jsx path="/02-react/src/components/JobCard.jsx" 
import { useState } from "react"

export function JobCard({ job }) {
  const [isApplied, setIsApplied] = useState(false)

  const handleApplyClick = () => {
    setIsApplied(true)
  }

  const buttonClasses = isApplied ? 'button-apply-job is-applied' : 'button-apply-job'
  const buttonText = isApplied ? 'Aplicado' : 'Aplicar'

  return (
    <article 
      className="job-listing-card"
      data-modalidad={job.data.modalidad}
      data-nivel={job.data.nivel}
      data-technology={job.data.technology}
    >
      <div>
        <h3>{job.titulo}</h3>
        <small>{job.empresa} | {job.ubicacion}</small>
        <p>{job.descripcion}</p>
      </div>
      <button className={buttonClasses} onClick={handleApplyClick}>{buttonText}</button>
    </article>
  )
}
```

## /02-react/src/components/JobListings.jsx

```jsx path="/02-react/src/components/JobListings.jsx" 
import { JobCard } from './JobCard.jsx'

export function JobListings ({ jobs }) {
  return (
    <>
      <div className="jobs-listings">
        {
          jobs.length === 0 && (
            <p style={{ textAlign: 'center', padding: '1rem', textWrap: 'balance' }}>No se han encontrado empleos que coincidan con los criterios de búsqueda.</p>
          )
        }
        
        {jobs.map(job => (
          <JobCard key={job.id} job={job} />
        ))}
      </div>
    </>
  )
}
```

## /02-react/src/components/Link.jsx

```jsx path="/02-react/src/components/Link.jsx" 
import { useRouter } from "../hooks/useRouter"

export function Link ({ href, children, ...restOfProps }) {
  const { navigateTo } = useRouter()

  const handleClick = (event) => {
    event.preventDefault()
    navigateTo(href)
  }

  return (
    <a href={href} {...restOfProps} onClick={handleClick}>
      {children}
    </a>
  )
}
```

## /02-react/src/components/Pagination.jsx

```jsx path="/02-react/src/components/Pagination.jsx" 
import styles from './Pagination.module.css'

export function Pagination ({ currentPage = 1, totalPages = 10, onPageChange }) {
  // generar un array de páginas a mostrar
  const pages = Array.from({ length: totalPages }, (_, i) => i + 1)

  const isFirstPage = currentPage === 1
  const isLastPage = currentPage === totalPages

  const stylePrevButton = isFirstPage ? { pointerEvents: 'none', opacity: 0.5 } : {}
  const styleNextButton = isLastPage ? { pointerEvents: 'none', opacity: 0.5 } : {}

  const handlePrevClick = (event) => {
    event.preventDefault()
    if (isFirstPage === false) {
      onPageChange(currentPage - 1)
    }
  }

  const handleNextClick = (event) => {
    event.preventDefault()
    if (isLastPage === false) {
      onPageChange(currentPage + 1)
    }
  }

  const handleChangePage = (event) => {
    event.preventDefault()
    const page = Number(event.target.dataset.page)

    if (page !== currentPage) {
      onPageChange(page)
    }
  }

  const buildPageUrl = (page) => {
    const url = new URL(window.location)
    url.searchParams.set('page', page)
    return `${url.pathname}?${url.searchParams.toString()}`
  }

  return (
    <nav className={styles.pagination}>
      
      <a href={buildPageUrl(currentPage - 1)} style={stylePrevButton} onClick={handlePrevClick}>
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
          strokeLinecap="round" strokeLinejoin="round">
          <path stroke="none" d="M0 0h24v24H0z" fill="none" />
          <path d="M15 6l-6 6l6 6" />
        </svg>
      </a>
      

      {pages.map((page) => (
        <a
          key={page}
          data-page={page}
          href={buildPageUrl(page)}
          className={currentPage === page ? styles.isActive : ''}
          onClick={handleChangePage}
        >
          {page}
        </a>
      ))}

      <a href={buildPageUrl(currentPage + 1)} style={styleNextButton} onClick={handleNextClick}>
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"
          strokeLinecap="round" strokeLinejoin="round"
          className="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right">
          <path stroke="none" d="M0 0h24v24H0z" fill="none" />
          <path d="M9 6l6 6l-6 6" />
        </svg>
      </a>

      
    </nav>
  )
}
```

## /02-react/src/components/Pagination.module.css

```css path="/02-react/src/components/Pagination.module.css" 
.pagination {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin-block: 2rem;

  a {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    text-decoration: none;
    color: var(--text-muted);
    border-radius: 0.375rem;
    transition: all .3s;

    &:hover, &:focus {
      background-color: #fff;
    }

    &:active {
      transform: scale(0.90);
    }
  }
}

.isActive {
  background-color: var(--primary-light);
  color: white;
  pointer-events: none;
}
```

## /02-react/src/components/Route.jsx

```jsx path="/02-react/src/components/Route.jsx" 
import { useRouter } from "../hooks/useRouter";

export function Route ({ path, component: Component }) {
  const { currentPath } = useRouter()
  if (currentPath !== path) return null

  return <Component />
}
```

## /02-react/src/components/SearchFormSection.jsx

```jsx path="/02-react/src/components/SearchFormSection.jsx" 
import { useId, useState, useRef } from "react"

const useSearchForm = ({ idTechnology, idLocation, idExperienceLevel, idText, onSearch, onTextFilter }) => {
  const timeoutId = useRef(null)
  const [searchText, setSearchText] = useState("")

  const handleSubmit = (event) => {
    event.preventDefault()
    
    const formData = new FormData(event.currentTarget)
    
    if (event.target.name === idText) {
      return // ya lo manejamos en onChange
    }

    const filters = {
      technology: formData.get(idTechnology),
      location: formData.get(idLocation),
      experienceLevel: formData.get(idExperienceLevel)
    }

    onSearch(filters)
  }

  const handleTextChange = (event) => {
    const text = event.target.value
    setSearchText(text) // actualizamos el input inmediatamente

    // Debounce: Cancelar el timeout anterior
    if (timeoutId.current) {
      clearTimeout(timeoutId.current)
    }

    timeoutId.current = setTimeout(() => {
      onTextFilter(text)
    }, 500)
  }

  return {
    searchText,
    handleSubmit,
    handleTextChange
  }
}

export function SearchFormSection ({ onTextFilter, onSearch, initialText }) {
  const idText = useId()
  const idTechnology = useId()
  const idLocation = useId()
  const idExperienceLevel = useId()

  const inputRef = useRef()

  const {
    handleSubmit,
    handleTextChange
  } = useSearchForm({ idTechnology, idLocation, idExperienceLevel, idText, onSearch, onTextFilter })

  const handleClearInput = (event) => {
    event.preventDefault()

    inputRef.current.value = ""
    onTextFilter("")
  }

  return (
    <section className="jobs-search">
      <h1>Encuentra tu próximo trabajo</h1>
      <p>Explora miles de oportunidades en el sector tecnológico.</p>

      <form onChange={handleSubmit} id="empleos-search-form" role="search">

        <div className="search-bar">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
            stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"
            className="icon icon-tabler icons-tabler-outline icon-tabler-search">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
            <path d="M21 21l-6 -6" />
          </svg>
          
          <input
            ref={inputRef}
            name={idText} id="empleos-search-input" type="text"
            placeholder="Buscar trabajos, empresas o habilidades"
            onChange={handleTextChange}
            defaultValue={initialText}
          />

          <button onClick={handleClearInput}>
           ✖︎
          </button>
        </div>

        <div className="search-filters">
          <select name={idTechnology} id="filter-technology">
            <option value="">Tecnología</option>
            <optgroup label="Tecnologías populares">
              <option value="javascript">JavaScript</option>
              <option value="python">Python</option>
              <option value="react">React</option>
              <option value="nodejs">Node.js</option>
            </optgroup>
            <option value="java">Java</option>
            <hr />
            <option value="csharp">C#</option>
            <option value="c">C</option>
            <option value="c++">C++</option>
            <hr />
            <option value="ruby">Ruby</option>
            <option value="php">PHP</option>
          </select>

          <select name={idLocation} id="filter-location">
            <option value="">Ubicación</option>
            <option value="remoto">Remoto</option>
            <option value="cdmx">Ciudad de México</option>
            <option value="guadalajara">Guadalajara</option>
            <option value="monterrey">Monterrey</option>
            <option value="barcelona">Barcelona</option>
          </select>

          <select name={idExperienceLevel} id="filter-experience-level">
            <option value="">Nivel de experiencia</option>
            <option value="junior">Junior</option>
            <option value="mid">Mid-level</option>
            <option value="senior">Senior</option>
            <option value="lead">Lead</option>
          </select>
        </div>
      </form>

      <span id="filter-selected-value"></span>
    </section>
  )
}
```

## /02-react/src/data.json

```json path="/02-react/src/data.json" 
[
  {
    "id": "7a4d1d8b-1e45-4d8c-9f1a-8c2f9a9121a4",
    "titulo": "Desarrollador de Software Senior",
    "empresa": "Tech Solutions Inc.",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un ingeniero de software con experiencia en desarrollo web y conocimientos en JavaScript, React y Node.js. El candidato ideal debe ser capaz de trabajar en equipo y tener buenas habilidades de comunicación.",
    "data": {
      "technology": "javascript",
      "modalidad": "remoto",
      "nivel": "senior"
    }
  },
  {
    "id": "d35b2c89-5d60-4f26-b19a-6cfb2f1a0f57",
    "titulo": "Analista de Datos",
    "empresa": "Data Driven Co.",
    "ubicacion": "Ciudad de México",
    "descripcion": "Estamos buscando un analista de datos con experiencia en el manejo de grandes conjuntos de datos y herramientas de visualización. Se requiere conocimiento en SQL, Python y R.",
    "data": {
      "technology": "python",
      "modalidad": "cdmx",
      "nivel": "junior"
    }
  },
  {
    "id": "e31f9a92-61d7-4b7a-b3a2-91e8c1f40b2d",
    "titulo": "Desarrollador de Aplicaciones Móviles",
    "empresa": "Mobile Apps Ltd.",
    "ubicacion": "Guadalajara",
    "descripcion": "Buscamos un desarrollador de aplicaciones móviles con experiencia en iOS y/o Android. El candidato debe tener conocimientos en Swift, Kotlin y el desarrollo de interfaces de usuario.",
    "data": {
      "technology": "mobile",
      "modalidad": "guadalajara",
      "nivel": "mid-level"
    }
  },
  {
    "id": "f62d8a34-923a-4ac2-9b0b-14e0ac2f5405",
    "titulo": "Ingeniero de DevOps",
    "empresa": "Cloud Services SA",
    "ubicacion": "Remoto",
    "descripcion": "Estamos buscando un ingeniero de DevOps con experiencia en la gestión de infraestructuras en la nube, automatización de procesos y herramientas de integración continua. Se requiere conocimiento en AWS, Azure o GCP.",
    "data": {
      "technology": "mobile",
      "modalidad": "remoto",
      "nivel": "mid-level"
    }
  },
  {
    "id": "a9f31a8e-ec38-4fd3-9114-88cc6d37a92b",
    "titulo": "Diseñador UX/UI",
    "empresa": "Creative Minds Studio",
    "ubicacion": "Barcelona",
    "descripcion": "Estamos buscando un diseñador UX/UI con pasión por crear experiencias digitales excepcionales. Se requiere experiencia en Figma, diseño centrado en el usuario y colaboración con equipos de desarrollo.",
    "data": {
      "technology": "mobile",
      "modalidad": "barcelona",
      "nivel": "mid-level"
    }
  },
  {
    "id": "c1b65b42-68c5-4f1c-a8c2-8d52c5a7a5d1",
    "titulo": "Administrador de Bases de Datos",
    "empresa": "Secure Data Corp.",
    "ubicacion": "Buenos Aires",
    "descripcion": "Se busca un administrador de bases de datos con experiencia en PostgreSQL y MySQL. El candidato deberá asegurar la disponibilidad, seguridad y rendimiento de las bases de datos de producción.",
    "data": {
      "technology": "mobile",
      "modalidad": "bsas",
      "nivel": "mid-level"
    }
  },
  {
    "id": "bb8f2a99-6a20-4f9e-912a-16f54a49b8c3",
    "titulo": "Especialista en Ciberseguridad",
    "empresa": "SafeNet Solutions",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un especialista en ciberseguridad con conocimientos en protección de infraestructuras, análisis de vulnerabilidades y respuesta ante incidentes. Se valorará experiencia con SIEM y certificaciones de seguridad.",
    "data": {
      "technology": "mobile",
      "modalidad": "remoto",
      "nivel": "mid-level"
    }
  },
  {
    "id": "fe7b2c54-4f47-4e2b-9e87-2b5413a6b24f",
    "titulo": "Product Manager",
    "empresa": "NextGen Technologies",
    "ubicacion": "Madrid",
    "descripcion": "Estamos buscando un Product Manager con experiencia en la definición y lanzamiento de productos digitales. Se requiere capacidad analítica, liderazgo y conocimiento en metodologías ágiles.",
    "data": {
      "technology": "mobile",
      "modalidad": "madrid",
      "nivel": "senior"
    }
  },
  {
    "id": "a71f7a92-56d9-4b42-9f16-cb29b40e5f2c",
    "titulo": "Frontend Developer",
    "empresa": "Bright Web Studio",
    "ubicacion": "Valencia",
    "descripcion": "Buscamos un desarrollador frontend con experiencia en React, TypeScript y Tailwind CSS. Se valorará conocimiento en optimización de rendimiento y accesibilidad web.",
    "data": {
      "technology": "react",
      "modalidad": "valencia",
      "nivel": "mid-level"
    }
  },
  {
    "id": "f91e4c7b-3840-43da-8ad7-3a52e2a8cf1d",
    "titulo": "Backend Developer",
    "empresa": "APIWorks",
    "ubicacion": "Bogotá",
    "descripcion": "Estamos buscando un desarrollador backend con experiencia en Node.js, Express y bases de datos NoSQL. Se requiere conocimiento en arquitectura de microservicios.",
    "data": {
      "technology": "node",
      "modalidad": "bogota",
      "nivel": "mid"
    }
  },
  {
    "id": "b65a3c9f-b174-4d86-b8a2-9cf9b1e13a22",
    "titulo": "Ingeniero de Machine Learning",
    "empresa": "AI Labs",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un ingeniero de machine learning con experiencia en modelos de predicción y procesamiento de datos. Se valorará conocimiento en TensorFlow, PyTorch y MLOps.",
    "data": {
      "technology": "python",
      "modalidad": "remoto",
      "nivel": "senior"
    }
  },
  {
    "id": "e8d13c45-36cb-46cf-8f9d-8a0a7b532b8b",
    "titulo": "QA Automation Engineer",
    "empresa": "Quality First",
    "ubicacion": "Lima",
    "descripcion": "Se busca ingeniero de QA con experiencia en automatización de pruebas utilizando herramientas como Selenium, Cypress o Playwright. Conocimiento deseado en CI/CD.",
    "data": {
      "technology": "mobile",
      "modalidad": "lima",
      "nivel": "mid-level"
    }
  },
  {
    "id": "d2e93b8a-0b41-4d09-9a52-36a0f418d493",
    "titulo": "Administrador de Sistemas",
    "empresa": "InfraTech Global",
    "ubicacion": "Santiago de Chile",
    "descripcion": "Buscamos un administrador de sistemas con experiencia en Linux, Docker y monitoreo de servidores. Conocimiento en scripting Bash o Python será un plus.",
    "data": {
      "technology": "mobile",
      "modalidad": "santiago",
      "nivel": "mid-level"
    }
  },
  {
    "id": "cc0c1fae-4e85-4e2c-9b02-f12f9df8a2c9",
    "titulo": "Scrum Master",
    "empresa": "Agile Minds",
    "ubicacion": "Madrid",
    "descripcion": "Estamos buscando un Scrum Master con experiencia en metodologías ágiles y gestión de equipos multidisciplinarios. Certificación Scrum Master deseable.",
    "data": {
      "technology": "mobile",
      "modalidad": "madrid",
      "nivel": "mid-level"
    }
  },
  {
    "id": "a2f1d8c6-b72c-45f5-bcc5-5c1d1f39b0b1",
    "titulo": "Soporte Técnico Nivel 2",
    "empresa": "HelpDesk Pro",
    "ubicacion": "Monterrey",
    "descripcion": "Buscamos un técnico de soporte con habilidades en resolución de incidencias, redes y sistemas operativos. Se requiere atención al detalle y excelente trato con el usuario.",
    "data": {
      "technology": "mobile",
      "modalidad": "monterrey",
      "nivel": "junior"
    }
  }
]


```

## /02-react/src/hooks/useRouter.jsx

```jsx path="/02-react/src/hooks/useRouter.jsx" 
import { useEffect, useState } from 'react'

export function useRouter() {
  const [currentPath, setCurrentPath] = useState(window.location.pathname)

  useEffect(() => {
    const handleLocationChange = () => {
      setCurrentPath(window.location.pathname)
    }

    window.addEventListener('popstate', handleLocationChange)
  }, [])

  function navigateTo(path) {
    window.history.pushState({}, '', path)
    window.dispatchEvent(new PopStateEvent('popstate')) 
  }

  return {
    currentPath,
    navigateTo
  }
}
```

## /02-react/src/index.css

```css path="/02-react/src/index.css" 
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}






:root {
  --primary: #3b4550;
  --primary-dark: #0a3d66;
  --primary-hover: #0d5ba8;
  --primary-light: #09f;
  --background: #06182a;
  --text-primary: #ffffff;
  --text-secondary: #cbd5e1;
  --text-muted: #94a3b8;
  --border: rgba(255, 255, 255, 0.1);
  --card-bg: #1e293b;
  --shadow: rgba(0, 0, 0, 0.3);
  --input-bg: #1e293b;
  --hsla-example: hsla(210, 100%, 56%, 1);
}










@font-face {
  font-family: 'Inter Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(https://cdn.jsdelivr.net/fontsource/fonts/inter:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}










body {
  font-family: system-ui;
  background-color: var(--background);
  color: var(--text-primary);
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  line-height: 1.6;
}



select {
  padding: 0.625rem 1.25rem;
  background-color: #242d3a;
  border: 0;
  border-radius: 0.5rem;
  font-size: 0.875rem;
  color: #ddd;
  cursor: pointer;
  transition: all 0.2s;
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.5rem center;
  padding-right: 2.5rem;

  &:hover {
    background-color: var(--primary-hover);
    color: white;
  }

  &:focus {
    outline: 2px solid #1173d4;
    outline-offset: 2px;
  }
}









/* Títulos principales */
h1 {
  font-size: 1.5rem;
  line-height: 1.2;
  text-wrap: balance;
  display: flex;
  align-items: center;
  gap: 0.5rem;

  svg {
    width: 2rem;
    height: 2rem;
    color: var(--primary-light);
  }
}

h1 + p {
  text-wrap: balance;
  margin-bottom: 2rem
}

/* Header */
header {
  border-bottom: 1px solid var(--border);
  background: var(--background);
  padding: .5rem 1rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;

  h2 {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--text-primary);
  }
}

/* Navegación del header a otras páginas */
nav {
  align-items: center;
  gap: 1rem;
  display: flex;

  a {
    text-decoration: none;
    color: var(--text-secondary);
    transition: color 0.2s;
    font-weight: 500;

    &:hover, &:focus {
      color: var(--primary);
      outline: none;
    }
  }
}

/* Botones del Header */
header div a {
  padding: 0.5rem 1rem;
  border-radius: 0.5rem;
  background-color: #334155;
  color: var(--text-primary);
  display: inline-block;
  font-size: 0.875rem;
  font-weight: 700;
  text-decoration: none;
  /* TODO */
}

/* Hero */
main > section:nth-child(1) {
  height: 500px;
  text-align: center;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  h1 {
    padding-top: 36px;
  }

  & > img {
    position: absolute;
    width: 100%;
    height: 100%;
    object-fit: cover;
    z-index: -1;
    left: 0;
    right: 0;
    mask-image: linear-gradient(to bottom, rgba(16, 25, 34, 1) 5%, rgba(16, 25, 34, 0) 80%);
  }
}

/* Formulario de Búsqueda */
form {
  max-width: 42rem;
  width: 100%;
  margin: 0 auto;
  padding-inline: 1rem;
}

form > div {
  display: flex;
  align-items: center;
  background-color: var(--input-bg);
  border-radius: 0.5rem;
  box-shadow: 0 10px 15px -3px var(--shadow);
  padding: 0.5rem;
  gap: 0.5rem;
}

form span {
  padding-left: 0.75rem;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

form input[type="text"] {
  flex: 1;
  background: transparent;
  border: none;
  outline: none;
  color: var(--text-primary);
  padding: 0.75rem 0.5rem;
  font-size: 1rem;
  font-family: inherit;
}

form input[type="text"]::placeholder {
  color: #64748b;
}

button {
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  background-color: var(--primary);
  color: white;
  font-weight: 400;
  border: none;
  cursor: pointer;
  transition: all 0.2s;
  font-family: inherit;
  font-size: 1rem;
  white-space: nowrap;

  &:hover, /* pseudo-clase que significa que el ratón está encima */
  &:focus { /* pseudo-clase que significa que el elemento tiene el foco */
    background-color: var(--primary-hover);
    outline: 2px solid white;
    outline-offset: 2px;
  }

  &:active { 
    transform: scale(0.90)
  }

  &:disabled {
    opacity: .5;
    pointer-events: none;
  }
}

/* Features Section */
main > section:nth-child(2) {
  padding-inline: 1rem;
  background-color: var(--background);
  padding-top: 2rem;

  & > header {
    gap: 2px;
    flex-direction: column;

    h2 {
      margin-bottom: 0;
    }

    p {
      opacity: .75;
    }
  }

  & > div {
    max-width: 1280px;
    margin: 0 auto;
  }
}

main > section:nth-child(2) h2 {
  font-size: 1.875rem;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 1rem;
}

main > section:nth-child(2) > div > div:first-child > p {
  font-size: 1.125rem;
  color: var(--text-muted);
  max-width: 42rem;
  margin: 0 auto;
}

/* Grid de Features */
main > section:nth-child(2) footer {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 16px
}

/* Feature Cards */
/* article {
  background-color: var(--card-bg);
  padding: 2rem;
  margin-bottom: 16px;
  border-radius: 0.5rem;                   
  box-shadow: 0px 1px 3px 0 var(--shadow); 

  svg {
    color: var(--primary-light);          
    background: rgba(0, 153, 255, 0.3);  
    border-radius: 9999px;               
    width: 64px;                         
    height: 64px;                        
    padding: 16px;                       
  }

  h3 {
    font-weight: 500;
  }

  p {
    color: var(--text-muted);
  }
} */

/* Footer */
footer {
  background-color: var(--background);
  border-top: 1px solid var(--border);
  text-align: center;
  padding: 16px;

  small {
    color: var(--text-muted);
    font-size: 0.875rem;
  }
}

article {
  background-color: var(--card-bg);
  padding: 2rem;
  margin-bottom: 16px;
  border-radius: 0.5rem;
  box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5);

  svg {
    color: var(--primary-light);
    background: rgba(0, 153, 255, .3);
    width: 64px;
    height: 64px;
    border-radius: 100%;
    padding: 16px;
  }

  p {
    color: var(--text-muted);
  }

  h3 {
    font-weight: 500;
  }
}

/* Jobs Search */
.jobs-search {
  margin: 0;
  padding: 0;
  height: auto !important;

  h1 {
    font-size: 2.5rem;
    margin-bottom: .25rem;
  }

  p {
    font-size: 1.125rem;
    color: var(--text-muted);
    margin-bottom: 1.5rem;
  }

  .search-bar {
    background: var(--input-bg);
    padding: .25rem .5rem;
    position: relative;
  }

  .search-filters {
    display: flex;
    flex-wrap: wrap;
    margin-top: .5rem;
  }

  form {
    width: 100%;
    max-width: 1280px;
  }

  form div {
    background: none;
    box-shadow: none;
    padding: 0;
  }
}

.jobs-listings {
  border: 1px solid rgba(255, 255, 255, .3);
  border-radius: 1rem;

  .job-listing-card {
    background: none;
    box-shadow: none;
    border-radius: 0;
    border-bottom: 1px solid rgba(255, 255, 255, .3);
    margin: 0;

    display: flex; /* valor original */
    align-items: start;
    gap: 1rem;

    &.is-hidden {
      display: none;
    }

    small {
      font-size: .875rem;
      opacity: .75;
    }

    p {
      margin-top: 0.5rem
    }

    &:last-child {
      border-bottom: none;
    }
  }
}

.button-apply-job {
  background: #09f;

  &.is-applied {
    background: #4caf50;
    pointer-events: none;
  }
}
```

## /02-react/src/main.jsx

```jsx path="/02-react/src/main.jsx" 
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <App />
)

```

## /02-react/src/pages/404.jsx

```jsx path="/02-react/src/pages/404.jsx" 
export function NotFoundPage () {
  return (
    <main>
      <h1>404 - Página no encontrada</h1>
      <p>Lo sentimos, la página que buscas no existe.</p>
    </main>
  )
}
```

## /02-react/src/pages/Home.jsx

```jsx path="/02-react/src/pages/Home.jsx" 
import { useRouter } from "../hooks/useRouter"

export function HomePage () {
  const { navigateTo } = useRouter()

  const handleSearch = (event) => {
    event.preventDefault()
    const formData = new FormData(event.target)
    const searchTerm = formData.get('search')
    
    const url = searchTerm
      ? `/search?text=${encodeURIComponent(searchTerm)}`
      : '/search'

    navigateTo(url)
  }

  return (
    <main>
      <section>
        <img src="./background.webp" width="200" />

        <h1>Encuentra el trabajo de tus sueños</h1>

        <p>Únete a la comunidad más grande de desarrolladores y encuentra tu próxima oportunidad.</p>

        <form role="search" onSubmit={handleSearch}>
          <div>
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none"
              stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"
              >
              <path stroke="none" d="M0 0h24v24H0z" fill="none" />
              <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
              <path d="M21 21l-6 -6" />
            </svg>

            <input
              name="search"
              required
              type="text"
              placeholder="Buscar empleos por título, habilidad o empresa"
            />

            <button type="submit">Buscar</button>
          </div>
        </form>
      </section>

      <section>

        <header>
          <h2>¿Por qué DevJobs?</h2>
          <p>DevJobs es la principal plataforma de búsqueda de empleo para desarrolladores. Conectamos a los mejores
            talentos con las empresas más innovadoras.</p>
        </header>

        <footer>
          <article>
            <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32"
              aria-hidden="true">
              <path
                d="M216,56H176V48a24,24,0,0,0-24-24H104A24,24,0,0,0,80,48v8H40A16,16,0,0,0,24,72V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V72A16,16,0,0,0,216,56ZM96,48a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96ZM216,72v41.61A184,184,0,0,1,128,136a184.07,184.07,0,0,1-88-22.38V72Zm0,128H40V131.64A200.19,200.19,0,0,0,128,152a200.25,200.25,0,0,0,88-20.37V200ZM104,112a8,8,0,0,1,8-8h32a8,8,0,0,1,0,16H112A8,8,0,0,1,104,112Z">
              </path>
            </svg>
            <h3>Encuentra el trabajo de tus sueños</h3>
            <p>Busca miles de empleos de las mejores empresas de todo el mundo.</p>
          </article>

          <article>
            <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32"
              aria-hidden="true">
              <path
                d="M117.25,157.92a60,60,0,1,0-66.5,0A95.83,95.83,0,0,0,3.53,195.63a8,8,0,1,0,13.4,8.74,80,80,0,0,1,134.14,0,8,8,0,0,0,13.4-8.74A95.83,95.83,0,0,0,117.25,157.92ZM40,108a44,44,0,1,1,44,44A44.05,44.05,0,0,1,40,108Zm210.14,98.7a8,8,0,0,1-11.07-2.33A79.83,79.83,0,0,0,172,168a8,8,0,0,1,0-16,44,44,0,1,0-16.34-84.87,8,8,0,1,1-5.94-14.85,60,60,0,0,1,55.53,105.64,95.83,95.83,0,0,1,47.22,37.71A8,8,0,0,1,250.14,206.7Z">
              </path>
            </svg>
            <h3>Conecta con las mejores empresas</h3>
            <p>Conecta con empresas que están contratando por tus habilidades.</p>
          </article>

          <article>
            <svg fill="currentColor" height="32" viewBox="0 0 256 256" width="32"
              aria-hidden="true">
              <path
                d="M240,208H224V96a16,16,0,0,0-16-16H144V32a16,16,0,0,0-24.88-13.32L39.12,72A16,16,0,0,0,32,85.34V208H16a8,8,0,0,0,0,16H240a8,8,0,0,0,0-16ZM208,96V208H144V96ZM48,85.34,128,32V208H48ZM112,112v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm-32,0v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm0,56v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Zm32,0v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Z">
              </path>
            </svg>
            <h3>Obtén el salario que mereces</h3>
            <p>Obtén el salario que mereces con nuestra calculadora de salarios.</p>
          </article>
        </footer>

      </section>
    </main>
  )
}
```

## /02-react/src/pages/Search.jsx

```jsx path="/02-react/src/pages/Search.jsx" 
import { useEffect, useState } from 'react'

import { Pagination } from '../components/Pagination.jsx'
import { SearchFormSection } from '../components/SearchFormSection.jsx'
import { JobListings } from '../components/JobListings.jsx'
import { useRouter } from '../hooks/useRouter.jsx'

const RESULTS_PER_PAGE = 4

const useFilters = () => {
  const [filters, setFilters] = useState(() => {
    const params = new URLSearchParams(window.location.search)
    return {
      technology: params.get('technology') || '',
      location: params.get('type') || '',
      experienceLevel: params.get('level') || ''
    }
  })
  const [textToFilter, setTextToFilter] = useState(() => {
    const params = new URLSearchParams(window.location.search)
    return params.get('text') || ''
  })
  const [currentPage, setCurrentPage] = useState(() => {
    const params = new URLSearchParams(window.location.search)
    const page = Number(params.get('page'))
    return Number.isNaN(page) ? page : 1
  })

  const [jobs, setJobs] = useState([])
  const [total, setTotal] = useState(0)
  const [loading, setLoading] = useState(true)

  const { navigateTo } = useRouter()

  useEffect(() => {
    async function fetchJobs() {
      try {
        setLoading(true)

        const params = new URLSearchParams()
        if (textToFilter) params.append('text', textToFilter)
        if (filters.technology) params.append('technology', filters.technology)
        if (filters.location) params.append('type', filters.location)
        if (filters.experienceLevel) params.append('level', filters.experienceLevel)

        const offset = (currentPage - 1) * RESULTS_PER_PAGE
        params.append('limit', RESULTS_PER_PAGE)
        params.append('offset', offset)

        const queryParams = params.toString()
      
        const response = await fetch(`https://jscamp-api.vercel.app/api/jobs?${queryParams}`)
        const json = await response.json()

        setJobs(json.data)
        setTotal(json.total)
      } catch (error) {
        console.error('Error fetching jobs:', error)
      } finally {
        setLoading(false)
      }
    }

    fetchJobs()
  }, [filters, currentPage, textToFilter])

  useEffect(() => {
    const params = new URLSearchParams()

    if (textToFilter) params.append('text', textToFilter)
    if (filters.technology) params.append('technology', filters.technology)
    if (filters.location) params.append('type', filters.location)
    if (filters.experienceLevel) params.append('level', filters.experienceLevel)

    if (currentPage > 1) params.append('page', currentPage)

    const newUrl = params.toString()
      ? `${window.location.pathname}?${params.toString()}`
      : window.location.pathname

    navigateTo(newUrl)
  }, [filters, currentPage, textToFilter, navigateTo])

  const totalPages = Math.ceil(total / RESULTS_PER_PAGE)

  const handlePageChange = (page) => {
    setCurrentPage(page)
  }

  const handleSearch = (filters) => {
    setFilters(filters)
    setCurrentPage(1)
  }

  const handleTextFilter = (newTextToFilter) => {
    setTextToFilter(newTextToFilter)
    setCurrentPage(1)
  }

  return {
    loading,
    jobs,
    total,
    totalPages,
    currentPage,
    textToFilter,
    handlePageChange,
    handleSearch,
    handleTextFilter
  }
}

export function SearchPage() {
  const {
    jobs,
    total,
    loading,
    totalPages,
    currentPage,
    textToFilter,
    handlePageChange,
    handleSearch,
    handleTextFilter
  } = useFilters()

  const title = loading
    ? `Cargando... - DevJobs`
    : `Resultados: ${total}, Página ${currentPage} - DevJobs`

  return (
    <main>
      <title>{title}</title>
      <meta name="description" content="Explora miles de oportunidades laborales en el sector tecnológico. Encuentra tu próximo empleo en DevJobs." />

      <SearchFormSection
        initialText={textToFilter}
        onSearch={handleSearch}
        onTextFilter={handleTextFilter}
      />

      <section>
        <h2 style={{ textAlign: 'center' }}>Resultados de búsqueda</h2>

        {
          loading ? <p>Cargando empleos...</p> : <JobListings jobs={jobs} />
        }
        <Pagination currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} />
      </section>
    </main>
  )
}

```

## /02-react/vite.config.js

```js path="/02-react/vite.config.js" 
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
})

```

## /03-router-and-zustand/.empty

```empty path="/03-router-and-zustand/.empty" 

```

## /03-router-and-zustand/.gitignore

```gitignore path="/03-router-and-zustand/.gitignore" 
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

```

## /03-router-and-zustand/README.md

# React + Vite

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.

Currently, two official plugins are available:

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh

## React Compiler

The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.

## Expanding the ESLint configuration

If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.


## /03-router-and-zustand/eslint.config.js

```js path="/03-router-and-zustand/eslint.config.js" 
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{js,jsx}'],
    extends: [
      js.configs.recommended,
      reactHooks.configs['recommended-latest'],
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: { jsx: true },
        sourceType: 'module',
      },
    },
    rules: {
      'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
    },
  },
])

```

## /03-router-and-zustand/index.html

```html path="/03-router-and-zustand/index.html" 
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>

</html>
```

## /03-router-and-zustand/package.json

```json path="/03-router-and-zustand/package.json" 
{
  "name": "02-react",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "react-router": "^7.9.6",
    "snarkdown": "^2.0.0",
    "zustand": "^5.0.8"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react-swc": "^4.1.0",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "vite": "^7.1.7"
  }
}

```

## /03-router-and-zustand/public/background.webp

Binary file available at https://raw.githubusercontent.com/midudev/jscamp/refs/heads/main/03-router-and-zustand/public/background.webp

## /03-router-and-zustand/public/vite.svg

```svg path="/03-router-and-zustand/public/vite.svg" 
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
```

## /03-router-and-zustand/src/App.jsx

```jsx path="/03-router-and-zustand/src/App.jsx" 
import { lazy, Suspense } from 'react' 
import { Routes, Route } from 'react-router'

import { Header } from './components/Header.jsx'
import { Footer } from './components/Footer.jsx'
import { ProtectedRoute } from './components/ProtectedRoute.jsx'

const HomePage = lazy(() => import('./pages/Home.jsx'))
const SearchPage = lazy(() => import('./pages/Search.jsx'))
const NotFoundPage = lazy(() => import('./pages/404.jsx'))
const JobDetail = lazy(() => import('./pages/Detail.jsx'))
const ProfilePage = lazy(() => import('./pages/ProfilePage.jsx'))
const Login = lazy(() => import('./pages/Login.jsx'))
const Register = lazy(() => import('./pages/Register.jsx'))

function App() {
  return (
    <>
      <Header />

      <Suspense fallback={<div style={{ maxWidth: '1280px', margin: '0 auto', padding: '0 1rem' }}>Cargando...</div>}>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/search" element={<SearchPage />} />
          <Route path="/jobs/:jobId" element={<JobDetail />} />
          <Route path="/profile" element={
            <ProtectedRoute redirectTo="/login">
              <ProfilePage />
            </ProtectedRoute>
          } />
          <Route path="*" element={<NotFoundPage />} />
          <Route path="/login" element={<Login />} />
          <Route path="/register" element={<Register />} />
        </Routes>
      </Suspense>
      <Footer />
    </>
  )
}

export default App

```

## /03-router-and-zustand/src/assets/react.svg

```svg path="/03-router-and-zustand/src/assets/react.svg" 
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
```

## /03-router-and-zustand/src/components/Footer.jsx

```jsx path="/03-router-and-zustand/src/components/Footer.jsx" 
export function Footer () {
  return (
    <footer>
      <small>&copy; 2025 DevJobs. Todos los derechos reservados.</small>
    </footer>
  )
}
```

## /03-router-and-zustand/src/components/Header.jsx

```jsx path="/03-router-and-zustand/src/components/Header.jsx" 
import { NavLink } from 'react-router'
import { Link } from './Link'
import { useAuthStore } from '../store/authStore'
import { useFavoritesStore } from '../store/favoritesStore'

export function Header () {
  const { isLoggedIn } = useAuthStore()
  const { countFavorites } = useFavoritesStore()

  const numberOfFavorites = countFavorites()

  return (
    <header>
      <Link href='/' style={{ textDecoration: 'none' }}>
        <h1 style={{ color: 'white' }}>
            <svg fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
              viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
              <polyline points="16 18 22 12 16 6"></polyline>
              <polyline points="8 6 2 12 8 18"></polyline>
            </svg>
            DevJobs
        </h1>
      </Link>

      <nav>
        <NavLink
          className={({ isActive }) => isActive ? 'nav-link-active' : ''}
          to="/search">Empleos</NavLink>
          {
            isLoggedIn && (
              <NavLink
                className={({ isActive }) => isActive ? 'nav-link-active' : ''}
                to="/profile">
                  Profile ❤️ {numberOfFavorites}
              </NavLink>
            )
          }
      </nav>

      <HeaderUserButton />

    </header>
  )
}

const HeaderUserButton = () => {
  const { isLoggedIn, login, logout } = useAuthStore()
  const { clearFavorites } = useFavoritesStore()

  const handleLogout = () => {
    logout()
    clearFavorites()
  }

  return isLoggedIn
    ? <button onClick={handleLogout}>Cerrar sesión</button>
    : <button onClick={login}>Iniciar sesión</button>
}
```

## /03-router-and-zustand/src/components/JobCard.jsx

```jsx path="/03-router-and-zustand/src/components/JobCard.jsx" 
import { useState } from "react"
import { Link } from "./Link"
import styles from './JobCard.module.css'
import { useFavoritesStore } from "../store/favoritesStore"
import { useAuthStore } from "../store/authStore"

function JobCardFavoriteButton ({ jobId }) {
  const { isLoggedIn } = useAuthStore()
  // suscríbete a TODA la store y extra TODA la store
  const { toggleFavorite, isFavorite } = useFavoritesStore()

  return (
    <button
      disabled={!isLoggedIn}
      onClick={() => toggleFavorite(jobId)}
      aria-label={isFavorite(jobId) ? 'Remove from favorites' : 'Add to favorites'}
    >
      {isFavorite(jobId) ? '❤️' : '🤍'}
    </button>
  )
}

function JobCardApplyButton ({ jobId }) {
  const [isApplied, setIsApplied] = useState(false)
  const { isLoggedIn } = useAuthStore()

  const buttonClasses = isApplied ? 'button-apply-job is-applied' : 'button-apply-job'
  const buttonText = isApplied ? 'Aplicado' : 'Aplicar'

  const handleApplyClick = () => {
    console.log('Aplicando al trabajo con id:', jobId)
    setIsApplied(true)
  }

  return (
    <button disabled={!isLoggedIn} className={buttonClasses} onClick={handleApplyClick}>{buttonText}</button>
  )
}

export function JobCard({ job }) {
  return (
    <article 
      className="job-listing-card"
      data-modalidad={job.data.modalidad}
      data-nivel={job.data.nivel}
      data-technology={job.data.technology}
    >
      <div>
        <h3>
          <Link className={styles.title} href={`/jobs/${job.id}`}>
            {job.titulo}
          </Link>
        </h3>
        <small>{job.empresa} | {job.ubicacion}</small>
        <p>{job.descripcion}</p>
      </div>
      <div className={styles.actions}>
        <Link href={`/jobs/${job.id}`} className={styles.details}>
          Ver detalles
        </Link>
        <JobCardApplyButton jobId={job.id} />
        <JobCardFavoriteButton jobId={job.id} />
      </div>
    </article>
  )
}
```

## /03-router-and-zustand/src/components/JobCard.module.css

```css path="/03-router-and-zustand/src/components/JobCard.module.css" 
.title {
  text-decoration: none;
  color: inherit;

  &:hover {
    text-decoration: underline;
  }
}

.actions {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  text-align: center;
}

.details {
  color: #09f;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1rem;
  text-decoration: none;

  &:hover {
    text-decoration: underline;
  }
}
```

## /03-router-and-zustand/src/components/JobListings.jsx

```jsx path="/03-router-and-zustand/src/components/JobListings.jsx" 
import { JobCard } from './JobCard.jsx'

export function JobListings ({ jobs }) {
  return (
    <>
      <div className="jobs-listings">
        {
          jobs.length === 0 && (
            <p style={{ textAlign: 'center', padding: '1rem', textWrap: 'balance' }}>No se han encontrado empleos que coincidan con los criterios de búsqueda.</p>
          )
        }
        
        {jobs.map(job => (
          <JobCard key={job.id} job={job} />
        ))}
      </div>
    </>
  )
}
```

## /03-router-and-zustand/src/components/Link.jsx

```jsx path="/03-router-and-zustand/src/components/Link.jsx" 
import { Link as NavLink } from 'react-router'

export function Link ({ href, children, ...restOfProps }) {
  return (
    <NavLink to={href} {...restOfProps}>
      {children}
    </NavLink>
  )
}
```

## /03-router-and-zustand/src/components/Pagination.jsx

```jsx path="/03-router-and-zustand/src/components/Pagination.jsx" 
import styles from './Pagination.module.css'

export function Pagination ({ currentPage = 1, totalPages = 10, onPageChange }) {
  // generar un array de páginas a mostrar
  const pages = Array.from({ length: totalPages }, (_, i) => i + 1)

  const isFirstPage = currentPage === 1
  const isLastPage = currentPage === totalPages

  const stylePrevButton = isFirstPage ? { pointerEvents: 'none', opacity: 0.5 } : {}
  const styleNextButton = isLastPage ? { pointerEvents: 'none', opacity: 0.5 } : {}

  const handlePrevClick = (event) => {
    event.preventDefault()
    if (isFirstPage === false) {
      onPageChange(currentPage - 1)
    }
  }

  const handleNextClick = (event) => {
    event.preventDefault()
    if (isLastPage === false) {
      onPageChange(currentPage + 1)
    }
  }

  const handleChangePage = (event) => {
    event.preventDefault()
    const page = Number(event.target.dataset.page)

    if (page !== currentPage) {
      onPageChange(page)
    }
  }

  const buildPageUrl = (page) => {
    const url = new URL(window.location)
    url.searchParams.set('page', page)
    return `${url.pathname}?${url.searchParams.toString()}`
  }

  return (
    <nav className={styles.pagination}>
      
      <a href={buildPageUrl(currentPage - 1)} style={stylePrevButton} onClick={handlePrevClick}>
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"
          strokeLinecap="round" strokeLinejoin="round">
          <path stroke="none" d="M0 0h24v24H0z" fill="none" />
          <path d="M15 6l-6 6l6 6" />
        </svg>
      </a>
      

      {pages.map((page) => (
        <a
          key={page}
          data-page={page}
          href={buildPageUrl(page)}
          className={currentPage === page ? styles.isActive : ''}
          onClick={handleChangePage}
        >
          {page}
        </a>
      ))}

      <a href={buildPageUrl(currentPage + 1)} style={styleNextButton} onClick={handleNextClick}>
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"
          strokeLinecap="round" strokeLinejoin="round"
          className="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right">
          <path stroke="none" d="M0 0h24v24H0z" fill="none" />
          <path d="M9 6l6 6l-6 6" />
        </svg>
      </a>

      
    </nav>
  )
}
```

## /03-router-and-zustand/src/components/Pagination.module.css

```css path="/03-router-and-zustand/src/components/Pagination.module.css" 
.pagination {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
  margin-block: 2rem;

  a {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 2.5rem;
    height: 2.5rem;
    text-decoration: none;
    color: var(--text-muted);
    border-radius: 0.375rem;
    transition: all .3s;

    &:hover, &:focus {
      background-color: #fff;
    }

    &:active {
      transform: scale(0.90);
    }
  }
}

.isActive {
  background-color: var(--primary-light);
  color: white;
  pointer-events: none;
}
```

## /03-router-and-zustand/src/components/ProtectedRoute.jsx

```jsx path="/03-router-and-zustand/src/components/ProtectedRoute.jsx" 
import { Navigate } from "react-router";
import { useAuthStore } from "../store/authStore";

export function ProtectedRoute({ children, redirectTo = '/login' }) {
  const { isLoggedIn } = useAuthStore()

  if (!isLoggedIn) {
    return <Navigate to={redirectTo} replace />
  }

  return children
}
```

## /03-router-and-zustand/src/components/Route.jsx

```jsx path="/03-router-and-zustand/src/components/Route.jsx" 
import { useRouter } from "../hooks/useRouter";

export function Route ({ path, component: Component }) {
  const { currentPath } = useRouter()
  if (currentPath !== path) return null

  return <Component />
}
```

## /03-router-and-zustand/src/components/SearchFormSection.jsx

```jsx path="/03-router-and-zustand/src/components/SearchFormSection.jsx" 
import { useId, useState, useRef } from "react"

const useSearchForm = ({ idTechnology, idLocation, idExperienceLevel, idText, onSearch, onTextFilter }) => {
  const timeoutId = useRef(null)
  const [searchText, setSearchText] = useState("")

  const handleSubmit = (event) => {
    event.preventDefault()
    
    const formData = new FormData(event.currentTarget)
    
    if (event.target.name === idText) {
      return // ya lo manejamos en onChange
    }

    const filters = {
      technology: formData.get(idTechnology),
      location: formData.get(idLocation),
      experienceLevel: formData.get(idExperienceLevel)
    }

    onSearch(filters)
  }

  const handleTextChange = (event) => {
    const text = event.target.value
    setSearchText(text) // actualizamos el input inmediatamente

    // Debounce: Cancelar el timeout anterior
    if (timeoutId.current) {
      clearTimeout(timeoutId.current)
    }

    timeoutId.current = setTimeout(() => {
      onTextFilter(text)
    }, 500)
  }

  return {
    searchText,
    handleSubmit,
    handleTextChange
  }
}

export function SearchFormSection ({ initialFilters, onTextFilter, onSearch, initialText }) {
  const idText = useId()
  const idTechnology = useId()
  const idLocation = useId()
  const idExperienceLevel = useId()

  const inputRef = useRef()

  const {
    handleSubmit,
    handleTextChange
  } = useSearchForm({ idTechnology, idLocation, idExperienceLevel, idText, onSearch, onTextFilter })

  const handleClearInput = (event) => {
    event.preventDefault()

    inputRef.current.value = ""
    onTextFilter("")
  }

  return (
    <section className="jobs-search">
      <h1>Encuentra tu próximo trabajo</h1>
      <p>Explora miles de oportunidades en el sector tecnológico.</p>

      <form onChange={handleSubmit} id="empleos-search-form" role="search">

        <div className="search-bar">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
            stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round"
            className="icon icon-tabler icons-tabler-outline icon-tabler-search">
            <path stroke="none" d="M0 0h24v24H0z" fill="none" />
            <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
            <path d="M21 21l-6 -6" />
          </svg>
          
          <input
            ref={inputRef}
            name={idText} id="empleos-search-input" type="text"
            placeholder="Buscar trabajos, empresas o habilidades"
            onChange={handleTextChange}
            defaultValue={initialText}
          />

          <button onClick={handleClearInput}>
           ✖︎
          </button>
        </div>

        <div className="search-filters">
          <select name={idTechnology} id="filter-technology" defaultValue={initialFilters.technology}>
            <option value="">Tecnología</option>
            <optgroup label="Tecnologías populares">
              <option value="javascript">JavaScript</option>
              <option value="python">Python</option>
              <option value="react">React</option>
              <option value="nodejs">Node.js</option>
            </optgroup>
            <option value="java">Java</option>
            <hr />
            <option value="csharp">C#</option>
            <option value="c">C</option>
            <option value="c++">C++</option>
            <hr />
            <option value="ruby">Ruby</option>
            <option value="php">PHP</option>
          </select>

          <select name={idLocation} id="filter-location" defaultValue={initialFilters.location}>
            <option value="">Ubicación</option>
            <option value="remoto">Remoto</option>
            <option value="cdmx">Ciudad de México</option>
            <option value="guadalajara">Guadalajara</option>
            <option value="monterrey">Monterrey</option>
            <option value="barcelona">Barcelona</option>
          </select>

          <select name={idExperienceLevel} id="filter-experience-level" defaultValue={initialFilters.experienceLevel}>
            <option value="">Nivel de experiencia</option>
            <option value="junior">Junior</option>
            <option value="mid">Mid-level</option>
            <option value="senior">Senior</option>
            <option value="lead">Lead</option>
          </select>
        </div>
      </form>

      <span id="filter-selected-value"></span>
    </section>
  )
}
```

## /03-router-and-zustand/src/context/AuthContext.jsx

```jsx path="/03-router-and-zustand/src/context/AuthContext.jsx" 
import { createContext, use, useState } from 'react'

export const AuthContext = createContext()

export function AuthProvider ({ children }) {
  const [isLoggedIn, setIsLoggedIn] = useState(false)

  const login = () => {
    setIsLoggedIn(true)
  }

  const logout = () => {
    setIsLoggedIn(false)
  }

  const value = {
    isLoggedIn,
    login,
    logout
  }

  return <AuthContext value={value}>
    {children}
  </AuthContext>
}

export function useAuth() {
  const context = use(AuthContext)

  if (context === undefined) {
    throw new Error('useAuth must be used within an AuthProvider')
  }

  return context
}
```

## /03-router-and-zustand/src/context/FavContext.jsx

```jsx path="/03-router-and-zustand/src/context/FavContext.jsx" 
import { createContext, use, useState } from 'react'

export const FavoritesContext = createContext()

export function FavoritesProvider ({ children }) {
  const [favorites, setFavorites] = useState([])

  const addFavorite = (job) => {
    setFavorites((prevFavorites) => [...prevFavorites, job])
  }

  const removeFavorite = (jobId) => {
    setFavorites((prevFavorites) =>
      prevFavorites.filter((job) => job.id !== jobId)
    )
  }

  const isFavorite = (jobId) => {
    return favorites.some((job) => job.id === jobId)
  }

  const value = {
    favorites,
    addFavorite,
    removeFavorite,
    isFavorite
  }

  return (
    <FavoritesContext value={value}>
      {children}
    </FavoritesContext>
  )
}

export function useFavorites() {
  const context = use(FavoritesContext)

  if (context === undefined) {
    throw new Error('useFavorites must be used within a FavoritesProvider')
  }

  return context
}
```

## /03-router-and-zustand/src/data.json

```json path="/03-router-and-zustand/src/data.json" 
[
  {
    "id": "7a4d1d8b-1e45-4d8c-9f1a-8c2f9a9121a4",
    "titulo": "Desarrollador de Software Senior",
    "empresa": "Tech Solutions Inc.",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un ingeniero de software con experiencia en desarrollo web y conocimientos en JavaScript, React y Node.js. El candidato ideal debe ser capaz de trabajar en equipo y tener buenas habilidades de comunicación.",
    "data": {
      "technology": "javascript",
      "modalidad": "remoto",
      "nivel": "senior"
    }
  },
  {
    "id": "d35b2c89-5d60-4f26-b19a-6cfb2f1a0f57",
    "titulo": "Analista de Datos",
    "empresa": "Data Driven Co.",
    "ubicacion": "Ciudad de México",
    "descripcion": "Estamos buscando un analista de datos con experiencia en el manejo de grandes conjuntos de datos y herramientas de visualización. Se requiere conocimiento en SQL, Python y R.",
    "data": {
      "technology": "python",
      "modalidad": "cdmx",
      "nivel": "junior"
    }
  },
  {
    "id": "e31f9a92-61d7-4b7a-b3a2-91e8c1f40b2d",
    "titulo": "Desarrollador de Aplicaciones Móviles",
    "empresa": "Mobile Apps Ltd.",
    "ubicacion": "Guadalajara",
    "descripcion": "Buscamos un desarrollador de aplicaciones móviles con experiencia en iOS y/o Android. El candidato debe tener conocimientos en Swift, Kotlin y el desarrollo de interfaces de usuario.",
    "data": {
      "technology": "mobile",
      "modalidad": "guadalajara",
      "nivel": "mid-level"
    }
  },
  {
    "id": "f62d8a34-923a-4ac2-9b0b-14e0ac2f5405",
    "titulo": "Ingeniero de DevOps",
    "empresa": "Cloud Services SA",
    "ubicacion": "Remoto",
    "descripcion": "Estamos buscando un ingeniero de DevOps con experiencia en la gestión de infraestructuras en la nube, automatización de procesos y herramientas de integración continua. Se requiere conocimiento en AWS, Azure o GCP.",
    "data": {
      "technology": "mobile",
      "modalidad": "remoto",
      "nivel": "mid-level"
    }
  },
  {
    "id": "a9f31a8e-ec38-4fd3-9114-88cc6d37a92b",
    "titulo": "Diseñador UX/UI",
    "empresa": "Creative Minds Studio",
    "ubicacion": "Barcelona",
    "descripcion": "Estamos buscando un diseñador UX/UI con pasión por crear experiencias digitales excepcionales. Se requiere experiencia en Figma, diseño centrado en el usuario y colaboración con equipos de desarrollo.",
    "data": {
      "technology": "mobile",
      "modalidad": "barcelona",
      "nivel": "mid-level"
    }
  },
  {
    "id": "c1b65b42-68c5-4f1c-a8c2-8d52c5a7a5d1",
    "titulo": "Administrador de Bases de Datos",
    "empresa": "Secure Data Corp.",
    "ubicacion": "Buenos Aires",
    "descripcion": "Se busca un administrador de bases de datos con experiencia en PostgreSQL y MySQL. El candidato deberá asegurar la disponibilidad, seguridad y rendimiento de las bases de datos de producción.",
    "data": {
      "technology": "mobile",
      "modalidad": "bsas",
      "nivel": "mid-level"
    }
  },
  {
    "id": "bb8f2a99-6a20-4f9e-912a-16f54a49b8c3",
    "titulo": "Especialista en Ciberseguridad",
    "empresa": "SafeNet Solutions",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un especialista en ciberseguridad con conocimientos en protección de infraestructuras, análisis de vulnerabilidades y respuesta ante incidentes. Se valorará experiencia con SIEM y certificaciones de seguridad.",
    "data": {
      "technology": "mobile",
      "modalidad": "remoto",
      "nivel": "mid-level"
    }
  },
  {
    "id": "fe7b2c54-4f47-4e2b-9e87-2b5413a6b24f",
    "titulo": "Product Manager",
    "empresa": "NextGen Technologies",
    "ubicacion": "Madrid",
    "descripcion": "Estamos buscando un Product Manager con experiencia en la definición y lanzamiento de productos digitales. Se requiere capacidad analítica, liderazgo y conocimiento en metodologías ágiles.",
    "data": {
      "technology": "mobile",
      "modalidad": "madrid",
      "nivel": "senior"
    }
  },
  {
    "id": "a71f7a92-56d9-4b42-9f16-cb29b40e5f2c",
    "titulo": "Frontend Developer",
    "empresa": "Bright Web Studio",
    "ubicacion": "Valencia",
    "descripcion": "Buscamos un desarrollador frontend con experiencia en React, TypeScript y Tailwind CSS. Se valorará conocimiento en optimización de rendimiento y accesibilidad web.",
    "data": {
      "technology": "react",
      "modalidad": "valencia",
      "nivel": "mid-level"
    }
  },
  {
    "id": "f91e4c7b-3840-43da-8ad7-3a52e2a8cf1d",
    "titulo": "Backend Developer",
    "empresa": "APIWorks",
    "ubicacion": "Bogotá",
    "descripcion": "Estamos buscando un desarrollador backend con experiencia en Node.js, Express y bases de datos NoSQL. Se requiere conocimiento en arquitectura de microservicios.",
    "data": {
      "technology": "node",
      "modalidad": "bogota",
      "nivel": "mid"
    }
  },
  {
    "id": "b65a3c9f-b174-4d86-b8a2-9cf9b1e13a22",
    "titulo": "Ingeniero de Machine Learning",
    "empresa": "AI Labs",
    "ubicacion": "Remoto",
    "descripcion": "Buscamos un ingeniero de machine learning con experiencia en modelos de predicción y procesamiento de datos. Se valorará conocimiento en TensorFlow, PyTorch y MLOps.",
    "data": {
      "technology": "python",
      "modalidad": "remoto",
      "nivel": "senior"
    }
  },
  {
    "id": "e8d13c45-36cb-46cf-8f9d-8a0a7b532b8b",
    "titulo": "QA Automation Engineer",
    "empresa": "Quality First",
    "ubicacion": "Lima",
    "descripcion": "Se busca ingeniero de QA con experiencia en automatización de pruebas utilizando herramientas como Selenium, Cypress o Playwright. Conocimiento deseado en CI/CD.",
    "data": {
      "technology": "mobile",
      "modalidad": "lima",
      "nivel": "mid-level"
    }
  },
  {
    "id": "d2e93b8a-0b41-4d09-9a52-36a0f418d493",
    "titulo": "Administrador de Sistemas",
    "empresa": "InfraTech Global",
    "ubicacion": "Santiago de Chile",
    "descripcion": "Buscamos un administrador de sistemas con experiencia en Linux, Docker y monitoreo de servidores. Conocimiento en scripting Bash o Python será un plus.",
    "data": {
      "technology": "mobile",
      "modalidad": "santiago",
      "nivel": "mid-level"
    }
  },
  {
    "id": "cc0c1fae-4e85-4e2c-9b02-f12f9df8a2c9",
    "titulo": "Scrum Master",
    "empresa": "Agile Minds",
    "ubicacion": "Madrid",
    "descripcion": "Estamos buscando un Scrum Master con experiencia en metodologías ágiles y gestión de equipos multidisciplinarios. Certificación Scrum Master deseable.",
    "data": {
      "technology": "mobile",
      "modalidad": "madrid",
      "nivel": "mid-level"
    }
  },
  {
    "id": "a2f1d8c6-b72c-45f5-bcc5-5c1d1f39b0b1",
    "titulo": "Soporte Técnico Nivel 2",
    "empresa": "HelpDesk Pro",
    "ubicacion": "Monterrey",
    "descripcion": "Buscamos un técnico de soporte con habilidades en resolución de incidencias, redes y sistemas operativos. Se requiere atención al detalle y excelente trato con el usuario.",
    "data": {
      "technology": "mobile",
      "modalidad": "monterrey",
      "nivel": "junior"
    }
  }
]


```

## /03-router-and-zustand/src/hooks/useRouter.jsx

```jsx path="/03-router-and-zustand/src/hooks/useRouter.jsx" 
import { useNavigate, useLocation } from 'react-router'

export function useRouter() {
  const navigate = useNavigate()
  const location = useLocation()

  function navigateTo(path) {
    navigate(path)
  }

  return {
    currentPath: location.pathname,
    navigateTo
  }
}
```

## /03-router-and-zustand/src/main.jsx

```jsx path="/03-router-and-zustand/src/main.jsx" 
import { BrowserRouter } from 'react-router'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
)

```

## /03-router-and-zustand/src/pages/404.jsx

```jsx path="/03-router-and-zustand/src/pages/404.jsx" 
export default function NotFoundPage () {
  return (
    <main>
      <h1>404 - Página no encontrada</h1>
      <p>Lo sentimos, la página que buscas no existe.</p>
    </main>
  )
}
```

## /03-router-and-zustand/src/pages/Auth.module.css

```css path="/03-router-and-zustand/src/pages/Auth.module.css" 
.container {
  min-height: calc(100vh - 200px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem 1rem;
}

.card {
  background: rgba(25, 25, 35, 0.8);
  border-radius: 1rem;
  padding: 2.5rem;
  width: 100%;
  max-width: 450px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}

.title {
  font-size: 2rem;
  font-weight: bold;
  color: var(--text-primary);
  margin-bottom: 0.5rem;
  text-align: center;
}

.subtitle {
  color: var(--text-muted);
  text-align: center;
  margin-bottom: 2rem;
}

.form {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.formGroup {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.label {
  color: var(--text-secondary);
  font-size: 0.875rem;
  font-weight: 500;
}

.input {
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 0.5rem;
  padding: 0.75rem 1rem;
  color: var(--text-primary);
  font-size: 1rem;
  transition: all 0.2s;
}

.input:focus {
  outline: none;
  border-color: #09f;
  background: rgba(255, 255, 255, 0.08);
}

.input::placeholder {
  color: var(--text-muted);
}

.submitButton {
  background: #09f;
  color: white;
  border: none;
  font-weight: 600;
  padding: 0.875rem;
  border-radius: 0.5rem;
  font-size: 1rem;
  cursor: pointer;
  transition: background 0.2s;
  margin-top: 0.5rem;
}

.submitButton:hover {
  background: #0088e6;
}

.footer {
  text-align: center;
  margin-top: 1.5rem;
  color: var(--text-muted);
  font-size: 0.875rem;
}

.link {
  color: #09f;
  text-decoration: none;
  font-weight: 500;
}

.link:hover {
  text-decoration: underline;
}

```

## /03-router-and-zustand/src/pages/Detail.module.css

```css path="/03-router-and-zustand/src/pages/Detail.module.css" 
/* pages/JobDetail.module.css */
.container {
  padding-block: 2rem;
  max-width: 64rem;
  margin: 0 auto;
}

.loading,
.error {
  padding-block: 5rem;
  text-align: center;
}

.loadingText {
  color: var(--text-muted);
}

.errorTitle {
  font-size: 1.5rem;
  font-weight: bold;
  color: var(--text-primary);
  margin-bottom: 1rem;
}

.errorButton {
  color: #09f;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1rem;
}

.errorButton:hover {
  text-decoration: underline;
}

.breadcrumb {
  font-size: 0.875rem;
  color: var(--text-muted);
  margin-bottom: 2rem;
}

.breadcrumbButton {
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  padding: 0;
  font-size: inherit;
  text-decoration: none;
}

.breadcrumbButton:hover {
  color: var(--text-primary);
}

.breadcrumbSeparator {
  margin: 0 0.5rem;
}

.breadcrumbCurrent {
  color: var(--text-primary);
}

.header {
  margin-bottom: 2rem;
}

.title {
  font-size: 2.25rem;
  font-weight: bold;
  color: var(--text-primary);
  margin-bottom: 1rem;
}

.meta {
  font-size: 1.25rem;
  color: var(--text-secondary);
}

.applyButton {
  width: 100%;
  background: #09f;
  color: white;
  border: none;
  font-weight: 600;
  padding: 0.75rem 2rem;
  border-radius: 0.5rem;
  margin-bottom: 3rem;
  transition: background 0.2s;
  cursor: pointer;
}

.applyButton:hover {
  background: #0088e6;
}

.applyButton:disabled {
  background: #666;
  cursor: not-allowed;
  opacity: 0.6;
}

.applyButton:disabled:hover {
  background: #666;
}

@media (min-width: 640px) {
  .applyButton {
    width: auto;
  }
}

.section {
  margin-bottom: 3rem;
}

.sectionTitle {
  font-size: 1.5rem;
  font-weight: bold;
  color: var(--text-primary);
  margin-bottom: 1rem;
}

.sectionContent {
  color: var(--text-secondary);
}

```

## /03-router-and-zustand/src/pages/Home.module.css

```css path="/03-router-and-zustand/src/pages/Home.module.css" 
.section {
  max-width: 1280px;
  margin: 1rem auto;
  padding: 5rem 1rem;

  h2 {
    font-size: 1.875rem;
    font-weight: 700;
    color: var(--text-primary);
    margin-bottom: 1rem;
  }

  p {
    font-size: 1.125rem;
    color: var(--text-muted);
    max-width: 42rem;
    margin: 0 auto;
    text-align: center;
  }

  header {
    display: flex;
    gap: 2px;
    flex-direction: column;
    margin-bottom: 1rem;

    p {
      opacity: .75;
    }
  }

  div {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 16px
  }

  article {
    text-align: center;
  }
}
```

## /03-router-and-zustand/src/pages/Search.module.css

```css path="/03-router-and-zustand/src/pages/Search.module.css" 
.searchResults {
  max-width: 1280px;
  margin: 1rem auto;
  padding: 1rem;
}
```

## /03-router-and-zustand/src/store/authStore.js

```js path="/03-router-and-zustand/src/store/authStore.js" 
import { create } from 'zustand'

export const useAuthStore = create((set) => ({
  // Estado
  isLoggedIn: false,

  // Acciones
  login: () => set({ isLoggedIn: true }),
  logout: () => set({ isLoggedIn: false})
}))
```


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!